@exagent/agent 0.1.17 → 0.1.19
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/dist/chunk-E5IE5WNT.mjs +4778 -0
- package/dist/chunk-IFPNUBXI.mjs +4666 -0
- package/dist/chunk-WDNMLAYM.mjs +4663 -0
- package/dist/chunk-XNP5C43A.mjs +4752 -0
- package/dist/chunk-ZLDWYPBF.mjs +4779 -0
- package/dist/cli.js +1818 -122
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +840 -4
- package/dist/index.d.ts +840 -4
- package/dist/index.js +1832 -109
- package/dist/index.mjs +25 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -37,12 +37,21 @@ __export(index_exports, {
|
|
|
37
37
|
DeepSeekAdapter: () => DeepSeekAdapter,
|
|
38
38
|
GoogleAdapter: () => GoogleAdapter,
|
|
39
39
|
GroqAdapter: () => GroqAdapter,
|
|
40
|
+
HYPERLIQUID_DOMAIN: () => HYPERLIQUID_DOMAIN,
|
|
41
|
+
HyperliquidClient: () => HyperliquidClient,
|
|
42
|
+
HyperliquidSigner: () => HyperliquidSigner,
|
|
43
|
+
HyperliquidWebSocket: () => HyperliquidWebSocket,
|
|
40
44
|
LLMConfigSchema: () => LLMConfigSchema,
|
|
41
45
|
LLMProviderSchema: () => LLMProviderSchema,
|
|
42
46
|
MarketDataService: () => MarketDataService,
|
|
43
47
|
MistralAdapter: () => MistralAdapter,
|
|
44
48
|
OllamaAdapter: () => OllamaAdapter,
|
|
45
49
|
OpenAIAdapter: () => OpenAIAdapter,
|
|
50
|
+
OrderManager: () => OrderManager,
|
|
51
|
+
PerpConfigSchema: () => PerpConfigSchema,
|
|
52
|
+
PerpOnboarding: () => PerpOnboarding,
|
|
53
|
+
PerpTradeRecorder: () => PerpTradeRecorder,
|
|
54
|
+
PositionManager: () => PositionManager,
|
|
46
55
|
RelayClient: () => RelayClient,
|
|
47
56
|
RelayConfigSchema: () => RelayConfigSchema,
|
|
48
57
|
RiskManager: () => RiskManager,
|
|
@@ -58,7 +67,10 @@ __export(index_exports, {
|
|
|
58
67
|
createSampleConfig: () => createSampleConfig,
|
|
59
68
|
decryptEnvFile: () => decryptEnvFile,
|
|
60
69
|
encryptEnvFile: () => encryptEnvFile,
|
|
70
|
+
fillHashToBytes32: () => fillHashToBytes32,
|
|
71
|
+
fillOidToBytes32: () => fillOidToBytes32,
|
|
61
72
|
getAllStrategyTemplates: () => getAllStrategyTemplates,
|
|
73
|
+
getNextNonce: () => getNextNonce,
|
|
62
74
|
getStrategyTemplate: () => getStrategyTemplate,
|
|
63
75
|
loadConfig: () => loadConfig,
|
|
64
76
|
loadSecureEnv: () => loadSecureEnv,
|
|
@@ -70,8 +82,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
70
82
|
|
|
71
83
|
// src/runtime.ts
|
|
72
84
|
var import_sdk2 = require("@exagent/sdk");
|
|
73
|
-
var
|
|
74
|
-
var
|
|
85
|
+
var import_viem6 = require("viem");
|
|
86
|
+
var import_chains4 = require("viem/chains");
|
|
87
|
+
var import_accounts5 = require("viem/accounts");
|
|
75
88
|
|
|
76
89
|
// src/llm/openai.ts
|
|
77
90
|
var import_openai = __toESM(require("openai"));
|
|
@@ -937,6 +950,26 @@ var RelayConfigSchema = import_zod.z.object({
|
|
|
937
950
|
apiUrl: import_zod.z.string().url(),
|
|
938
951
|
heartbeatIntervalMs: import_zod.z.number().min(5e3).default(3e4)
|
|
939
952
|
}).optional();
|
|
953
|
+
var PerpConfigSchema = import_zod.z.object({
|
|
954
|
+
/** Enable perp trading */
|
|
955
|
+
enabled: import_zod.z.boolean().default(false),
|
|
956
|
+
/** Hyperliquid REST API URL */
|
|
957
|
+
apiUrl: import_zod.z.string().url().default("https://api.hyperliquid.xyz"),
|
|
958
|
+
/** Hyperliquid WebSocket URL */
|
|
959
|
+
wsUrl: import_zod.z.string().default("wss://api.hyperliquid.xyz/ws"),
|
|
960
|
+
/** Builder address for fee collection (must have >= 100 USDC on HL) */
|
|
961
|
+
builderAddress: import_zod.z.string(),
|
|
962
|
+
/** Builder fee in tenths of basis points (100 = 10 bps = 0.10%) */
|
|
963
|
+
builderFeeTenthsBps: import_zod.z.number().min(0).max(500).default(100),
|
|
964
|
+
/** Private key for the perp relayer (calls recordPerpTrade on Base). Falls back to agent wallet. */
|
|
965
|
+
perpRelayerKey: import_zod.z.string().optional(),
|
|
966
|
+
/** Maximum leverage per position (default: 10) */
|
|
967
|
+
maxLeverage: import_zod.z.number().min(1).max(50).default(10),
|
|
968
|
+
/** Maximum notional position size in USD (default: 50000) */
|
|
969
|
+
maxNotionalUSD: import_zod.z.number().min(100).default(5e4),
|
|
970
|
+
/** Allowed perp instruments (e.g. ["ETH", "BTC", "SOL"]). If empty, all instruments allowed. */
|
|
971
|
+
allowedInstruments: import_zod.z.array(import_zod.z.string()).optional()
|
|
972
|
+
}).optional();
|
|
940
973
|
var AgentConfigSchema = import_zod.z.object({
|
|
941
974
|
// Identity (from on-chain registration)
|
|
942
975
|
agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
|
|
@@ -954,6 +987,8 @@ var AgentConfigSchema = import_zod.z.object({
|
|
|
954
987
|
vault: VaultConfigSchema.default({}),
|
|
955
988
|
// Relay configuration (command center)
|
|
956
989
|
relay: RelayConfigSchema,
|
|
990
|
+
// Perp trading configuration (Hyperliquid)
|
|
991
|
+
perp: PerpConfigSchema,
|
|
957
992
|
// Allowed tokens (addresses)
|
|
958
993
|
allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
|
|
959
994
|
});
|
|
@@ -1538,6 +1573,101 @@ var RiskManager = class {
|
|
|
1538
1573
|
isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
|
|
1539
1574
|
};
|
|
1540
1575
|
}
|
|
1576
|
+
// ============================================================
|
|
1577
|
+
// PERP RISK FILTERING
|
|
1578
|
+
// ============================================================
|
|
1579
|
+
/**
|
|
1580
|
+
* Filter perp trade signals through risk checks.
|
|
1581
|
+
* Reduce-only signals (closes) always pass.
|
|
1582
|
+
*
|
|
1583
|
+
* @param signals - Raw perp signals from strategy
|
|
1584
|
+
* @param positions - Current open positions on Hyperliquid
|
|
1585
|
+
* @param account - Current account equity and margin
|
|
1586
|
+
* @param maxLeverage - Maximum allowed leverage
|
|
1587
|
+
* @param maxNotionalUSD - Maximum notional per position
|
|
1588
|
+
* @returns Signals that pass risk checks
|
|
1589
|
+
*/
|
|
1590
|
+
filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD) {
|
|
1591
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1592
|
+
if (today !== this.lastResetDate) {
|
|
1593
|
+
this.dailyPnL = 0;
|
|
1594
|
+
this.dailyFees = 0;
|
|
1595
|
+
this.lastResetDate = today;
|
|
1596
|
+
}
|
|
1597
|
+
if (this.isDailyLossLimitHit(account.totalEquity)) {
|
|
1598
|
+
console.warn("Daily loss limit reached \u2014 blocking new perp trades");
|
|
1599
|
+
return signals.filter((s) => s.reduceOnly);
|
|
1600
|
+
}
|
|
1601
|
+
return signals.filter((signal) => this.validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD));
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Validate an individual perp signal.
|
|
1605
|
+
*/
|
|
1606
|
+
validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD) {
|
|
1607
|
+
if (signal.action === "hold") {
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
if (signal.reduceOnly || signal.action === "close_long" || signal.action === "close_short") {
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
if (signal.confidence < 0.5) {
|
|
1614
|
+
console.warn(`Perp signal confidence too low: ${signal.confidence} for ${signal.instrument}`);
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
if (signal.leverage > maxLeverage) {
|
|
1618
|
+
console.warn(`Perp signal leverage ${signal.leverage}x exceeds max ${maxLeverage}x for ${signal.instrument}`);
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
const signalNotional = signal.size * signal.price;
|
|
1622
|
+
if (signalNotional > maxNotionalUSD) {
|
|
1623
|
+
console.warn(`Perp signal notional $${signalNotional.toFixed(0)} exceeds max $${maxNotionalUSD} for ${signal.instrument}`);
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
const currentNotional = account.totalNotional;
|
|
1627
|
+
const projectedNotional = currentNotional + signalNotional;
|
|
1628
|
+
const projectedLeverage = account.totalEquity > 0 ? projectedNotional / account.totalEquity : 0;
|
|
1629
|
+
if (projectedLeverage > maxLeverage) {
|
|
1630
|
+
console.warn(
|
|
1631
|
+
`Perp signal would push aggregate leverage to ${projectedLeverage.toFixed(1)}x (max: ${maxLeverage}x) \u2014 blocked`
|
|
1632
|
+
);
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
const existingPos = positions.find((p) => p.instrument === signal.instrument);
|
|
1636
|
+
if (existingPos) {
|
|
1637
|
+
const liqProximity = this.calculateLiquidationProximity(existingPos);
|
|
1638
|
+
if (liqProximity > 0.7) {
|
|
1639
|
+
console.warn(
|
|
1640
|
+
`Position ${signal.instrument} liquidation proximity ${(liqProximity * 100).toFixed(0)}% \u2014 blocking new entry`
|
|
1641
|
+
);
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const requiredMargin = signalNotional / signal.leverage;
|
|
1646
|
+
if (requiredMargin > account.availableMargin) {
|
|
1647
|
+
console.warn(
|
|
1648
|
+
`Insufficient margin for ${signal.instrument}: need $${requiredMargin.toFixed(0)}, have $${account.availableMargin.toFixed(0)}`
|
|
1649
|
+
);
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Calculate liquidation proximity for a position (0.0 = safe, 1.0 = liquidated).
|
|
1656
|
+
*/
|
|
1657
|
+
calculateLiquidationProximity(pos) {
|
|
1658
|
+
if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) return 0;
|
|
1659
|
+
if (pos.size > 0) {
|
|
1660
|
+
if (pos.markPrice <= pos.liquidationPrice) return 1;
|
|
1661
|
+
const distance = pos.markPrice - pos.liquidationPrice;
|
|
1662
|
+
const total = pos.entryPrice - pos.liquidationPrice;
|
|
1663
|
+
return total > 0 ? 1 - distance / total : 0;
|
|
1664
|
+
} else {
|
|
1665
|
+
if (pos.markPrice >= pos.liquidationPrice) return 1;
|
|
1666
|
+
const distance = pos.liquidationPrice - pos.markPrice;
|
|
1667
|
+
const total = pos.liquidationPrice - pos.entryPrice;
|
|
1668
|
+
return total > 0 ? 1 - distance / total : 0;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1541
1671
|
};
|
|
1542
1672
|
|
|
1543
1673
|
// src/vault/manager.ts
|
|
@@ -2105,130 +2235,1488 @@ function openBrowser(url) {
|
|
|
2105
2235
|
}
|
|
2106
2236
|
}
|
|
2107
2237
|
|
|
2108
|
-
// src/
|
|
2109
|
-
var
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
client;
|
|
2114
|
-
llm;
|
|
2115
|
-
strategy;
|
|
2116
|
-
executor;
|
|
2117
|
-
riskManager;
|
|
2118
|
-
marketData;
|
|
2119
|
-
vaultManager;
|
|
2120
|
-
relay = null;
|
|
2121
|
-
isRunning = false;
|
|
2122
|
-
mode = "idle";
|
|
2123
|
-
configHash;
|
|
2124
|
-
cycleCount = 0;
|
|
2125
|
-
lastCycleAt = 0;
|
|
2126
|
-
lastPortfolioValue = 0;
|
|
2127
|
-
lastEthBalance = "0";
|
|
2128
|
-
processAlive = true;
|
|
2129
|
-
riskUniverse = 0;
|
|
2130
|
-
allowedTokens = /* @__PURE__ */ new Set();
|
|
2238
|
+
// src/perp/client.ts
|
|
2239
|
+
var HyperliquidClient = class {
|
|
2240
|
+
apiUrl;
|
|
2241
|
+
meta = null;
|
|
2242
|
+
assetIndexCache = /* @__PURE__ */ new Map();
|
|
2131
2243
|
constructor(config) {
|
|
2132
|
-
this.
|
|
2244
|
+
this.apiUrl = config.apiUrl;
|
|
2245
|
+
}
|
|
2246
|
+
// ============================================================
|
|
2247
|
+
// INFO API (read-only)
|
|
2248
|
+
// ============================================================
|
|
2249
|
+
/** Fetch perpetuals metadata (asset specs, names, indices) */
|
|
2250
|
+
async getMeta() {
|
|
2251
|
+
if (this.meta) return this.meta;
|
|
2252
|
+
const resp = await this.infoRequest({ type: "meta" });
|
|
2253
|
+
this.meta = resp.universe;
|
|
2254
|
+
this.meta.forEach((asset, idx) => {
|
|
2255
|
+
this.assetIndexCache.set(asset.name, idx);
|
|
2256
|
+
});
|
|
2257
|
+
return this.meta;
|
|
2258
|
+
}
|
|
2259
|
+
/** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
|
|
2260
|
+
async getAssetIndex(coin) {
|
|
2261
|
+
if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
|
|
2262
|
+
await this.getMeta();
|
|
2263
|
+
const idx = this.assetIndexCache.get(coin);
|
|
2264
|
+
if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
|
|
2265
|
+
return idx;
|
|
2266
|
+
}
|
|
2267
|
+
/** Get mid-market prices for all perpetuals */
|
|
2268
|
+
async getAllMids() {
|
|
2269
|
+
return this.infoRequest({ type: "allMids" });
|
|
2270
|
+
}
|
|
2271
|
+
/** Get clearinghouse state (positions, margin, equity) for a user */
|
|
2272
|
+
async getClearinghouseState(user) {
|
|
2273
|
+
return this.infoRequest({ type: "clearinghouseState", user });
|
|
2274
|
+
}
|
|
2275
|
+
/** Get user's recent fills */
|
|
2276
|
+
async getUserFills(user, startTime) {
|
|
2277
|
+
return this.infoRequest({
|
|
2278
|
+
type: "userFills",
|
|
2279
|
+
user,
|
|
2280
|
+
...startTime !== void 0 && { startTime }
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
/** Get user's fills in a time range */
|
|
2284
|
+
async getUserFillsByTime(user, startTime, endTime) {
|
|
2285
|
+
return this.infoRequest({
|
|
2286
|
+
type: "userFillsByTime",
|
|
2287
|
+
user,
|
|
2288
|
+
startTime,
|
|
2289
|
+
...endTime !== void 0 && { endTime }
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
/** Get user's open orders */
|
|
2293
|
+
async getOpenOrders(user) {
|
|
2294
|
+
return this.infoRequest({ type: "openOrders", user });
|
|
2295
|
+
}
|
|
2296
|
+
/** Get user's funding history */
|
|
2297
|
+
async getUserFunding(user, startTime) {
|
|
2298
|
+
return this.infoRequest({ type: "userFunding", user, startTime });
|
|
2299
|
+
}
|
|
2300
|
+
/** Check max approved builder fee for a user */
|
|
2301
|
+
async getMaxBuilderFee(user, builder) {
|
|
2302
|
+
const resp = await this.infoRequest({ type: "maxBuilderFee", user, builder });
|
|
2303
|
+
return resp;
|
|
2304
|
+
}
|
|
2305
|
+
/** Get L2 order book for a coin */
|
|
2306
|
+
async getL2Book(coin, depth) {
|
|
2307
|
+
return this.infoRequest({
|
|
2308
|
+
type: "l2Book",
|
|
2309
|
+
coin,
|
|
2310
|
+
...depth !== void 0 && { nSigFigs: depth }
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
// ============================================================
|
|
2314
|
+
// HIGH-LEVEL HELPERS
|
|
2315
|
+
// ============================================================
|
|
2316
|
+
/** Get parsed positions for a user address */
|
|
2317
|
+
async getPositions(user) {
|
|
2318
|
+
const state = await this.getClearinghouseState(user);
|
|
2319
|
+
return state.assetPositions.filter((p) => parseFloat(p.position.szi) !== 0).map((p) => this.parsePosition(p));
|
|
2320
|
+
}
|
|
2321
|
+
/** Get account summary (equity, margin, leverage) */
|
|
2322
|
+
async getAccountSummary(user) {
|
|
2323
|
+
const state = await this.getClearinghouseState(user);
|
|
2324
|
+
const crossMarginSummary = state.crossMarginSummary;
|
|
2325
|
+
const totalEquity = parseFloat(crossMarginSummary.accountValue);
|
|
2326
|
+
const totalNotional = parseFloat(crossMarginSummary.totalNtlPos);
|
|
2327
|
+
const totalMarginUsed = parseFloat(crossMarginSummary.totalMarginUsed);
|
|
2328
|
+
return {
|
|
2329
|
+
totalEquity,
|
|
2330
|
+
availableMargin: totalEquity - totalMarginUsed,
|
|
2331
|
+
totalMarginUsed,
|
|
2332
|
+
totalUnrealizedPnl: parseFloat(crossMarginSummary.totalRawUsd) - totalEquity,
|
|
2333
|
+
totalNotional,
|
|
2334
|
+
maintenanceMargin: totalMarginUsed * 0.5,
|
|
2335
|
+
// Approximate
|
|
2336
|
+
effectiveLeverage: totalEquity > 0 ? totalNotional / totalEquity : 0,
|
|
2337
|
+
cashBalance: parseFloat(state.crossMarginSummary.accountValue) - state.assetPositions.reduce(
|
|
2338
|
+
(sum, p) => sum + parseFloat(p.position.unrealizedPnl),
|
|
2339
|
+
0
|
|
2340
|
+
)
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
/** Get market data for a list of instruments */
|
|
2344
|
+
async getMarketData(instruments) {
|
|
2345
|
+
const mids = await this.getAllMids();
|
|
2346
|
+
const meta = await this.getMeta();
|
|
2347
|
+
return instruments.filter((inst) => mids[inst] !== void 0).map((inst) => {
|
|
2348
|
+
const midPrice = parseFloat(mids[inst]);
|
|
2349
|
+
const assetMeta = meta.find((m) => m.name === inst);
|
|
2350
|
+
return {
|
|
2351
|
+
instrument: inst,
|
|
2352
|
+
midPrice,
|
|
2353
|
+
bestBid: midPrice,
|
|
2354
|
+
// Approximate — use L2 book for exact
|
|
2355
|
+
bestAsk: midPrice,
|
|
2356
|
+
funding8h: 0,
|
|
2357
|
+
// Would need separate funding API call
|
|
2358
|
+
openInterest: 0,
|
|
2359
|
+
// Would need separate meta call
|
|
2360
|
+
volume24h: 0,
|
|
2361
|
+
priceChange24h: 0
|
|
2362
|
+
};
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
/** Convert fills to our PerpFill type */
|
|
2366
|
+
parseFill(fill) {
|
|
2367
|
+
return {
|
|
2368
|
+
oid: fill.oid,
|
|
2369
|
+
coin: fill.coin,
|
|
2370
|
+
side: fill.side,
|
|
2371
|
+
px: fill.px,
|
|
2372
|
+
sz: fill.sz,
|
|
2373
|
+
fee: fill.fee,
|
|
2374
|
+
time: fill.time,
|
|
2375
|
+
hash: fill.hash,
|
|
2376
|
+
isMaker: fill.startPosition !== fill.px,
|
|
2377
|
+
// Approximate maker detection
|
|
2378
|
+
builderFee: fill.builderFee,
|
|
2379
|
+
liquidation: fill.liquidation
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
// ============================================================
|
|
2383
|
+
// PRIVATE HELPERS
|
|
2384
|
+
// ============================================================
|
|
2385
|
+
parsePosition(ap) {
|
|
2386
|
+
const pos = ap.position;
|
|
2387
|
+
const size = parseFloat(pos.szi);
|
|
2388
|
+
const entryPrice = parseFloat(pos.entryPx || "0");
|
|
2389
|
+
const markPrice = parseFloat(pos.positionValue || "0") / Math.abs(size || 1);
|
|
2390
|
+
return {
|
|
2391
|
+
instrument: pos.coin,
|
|
2392
|
+
assetIndex: this.assetIndexCache.get(pos.coin) ?? -1,
|
|
2393
|
+
size,
|
|
2394
|
+
entryPrice,
|
|
2395
|
+
markPrice,
|
|
2396
|
+
unrealizedPnl: parseFloat(pos.unrealizedPnl),
|
|
2397
|
+
leverage: parseFloat(pos.leverage?.value || "1"),
|
|
2398
|
+
marginType: pos.leverage?.type === "isolated" ? "isolated" : "cross",
|
|
2399
|
+
liquidationPrice: parseFloat(pos.liquidationPx || "0"),
|
|
2400
|
+
notionalUSD: Math.abs(size) * markPrice,
|
|
2401
|
+
marginUsed: parseFloat(pos.marginUsed)
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
async infoRequest(body) {
|
|
2405
|
+
const resp = await fetch(`${this.apiUrl}/info`, {
|
|
2406
|
+
method: "POST",
|
|
2407
|
+
headers: { "Content-Type": "application/json" },
|
|
2408
|
+
body: JSON.stringify(body)
|
|
2409
|
+
});
|
|
2410
|
+
if (!resp.ok) {
|
|
2411
|
+
throw new Error(`Hyperliquid Info API error: ${resp.status} ${await resp.text()}`);
|
|
2412
|
+
}
|
|
2413
|
+
return resp.json();
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2417
|
+
// src/perp/signer.ts
|
|
2418
|
+
var import_viem3 = require("viem");
|
|
2419
|
+
var HYPERLIQUID_DOMAIN = {
|
|
2420
|
+
name: "HyperliquidSignTransaction",
|
|
2421
|
+
version: "1",
|
|
2422
|
+
chainId: 42161n,
|
|
2423
|
+
// Always Arbitrum chain ID
|
|
2424
|
+
verifyingContract: "0x0000000000000000000000000000000000000000"
|
|
2425
|
+
};
|
|
2426
|
+
var HYPERLIQUID_TYPES = {
|
|
2427
|
+
HyperliquidTransaction: [
|
|
2428
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
2429
|
+
{ name: "action", type: "string" },
|
|
2430
|
+
{ name: "nonce", type: "uint64" }
|
|
2431
|
+
]
|
|
2432
|
+
};
|
|
2433
|
+
var lastNonce = 0n;
|
|
2434
|
+
function getNextNonce() {
|
|
2435
|
+
const now = BigInt(Date.now());
|
|
2436
|
+
if (now <= lastNonce) {
|
|
2437
|
+
lastNonce = lastNonce + 1n;
|
|
2438
|
+
} else {
|
|
2439
|
+
lastNonce = now;
|
|
2440
|
+
}
|
|
2441
|
+
return lastNonce;
|
|
2442
|
+
}
|
|
2443
|
+
var HyperliquidSigner = class {
|
|
2444
|
+
constructor(walletClient) {
|
|
2445
|
+
this.walletClient = walletClient;
|
|
2133
2446
|
}
|
|
2134
2447
|
/**
|
|
2135
|
-
*
|
|
2448
|
+
* Sign an exchange action (order, cancel, etc.)
|
|
2449
|
+
*
|
|
2450
|
+
* @param action - The action object (will be JSON-serialized)
|
|
2451
|
+
* @param nonce - Nonce (defaults to current timestamp)
|
|
2452
|
+
* @returns Signature hex string
|
|
2136
2453
|
*/
|
|
2137
|
-
async
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2454
|
+
async signAction(action, nonce) {
|
|
2455
|
+
const actionNonce = nonce ?? getNextNonce();
|
|
2456
|
+
const actionStr = JSON.stringify(action);
|
|
2457
|
+
const account = this.walletClient.account;
|
|
2458
|
+
if (!account) throw new Error("Wallet client has no account");
|
|
2459
|
+
const signature = await this.walletClient.signTypedData({
|
|
2460
|
+
account,
|
|
2461
|
+
domain: HYPERLIQUID_DOMAIN,
|
|
2462
|
+
types: HYPERLIQUID_TYPES,
|
|
2463
|
+
primaryType: "HyperliquidTransaction",
|
|
2464
|
+
message: {
|
|
2465
|
+
hyperliquidChain: "Mainnet",
|
|
2466
|
+
action: actionStr,
|
|
2467
|
+
nonce: actionNonce
|
|
2468
|
+
}
|
|
2142
2469
|
});
|
|
2143
|
-
|
|
2144
|
-
const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
|
|
2145
|
-
if (!agent) {
|
|
2146
|
-
throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
|
|
2147
|
-
}
|
|
2148
|
-
console.log(`Agent verified: ${agent.name}`);
|
|
2149
|
-
await this.ensureWalletLinked();
|
|
2150
|
-
await this.loadTradingRestrictions();
|
|
2151
|
-
console.log(`Initializing LLM: ${this.config.llm.provider}`);
|
|
2152
|
-
this.llm = await createLLMAdapter(this.config.llm);
|
|
2153
|
-
const llmMeta = this.llm.getMetadata();
|
|
2154
|
-
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
2155
|
-
await this.syncConfigHash();
|
|
2156
|
-
this.strategy = await loadStrategy();
|
|
2157
|
-
this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
|
|
2158
|
-
this.riskManager = new RiskManager(this.config.trading);
|
|
2159
|
-
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
2160
|
-
await this.initializeVaultManager();
|
|
2161
|
-
await this.initializeRelay();
|
|
2162
|
-
console.log("Agent initialized successfully");
|
|
2470
|
+
return { signature, nonce: actionNonce };
|
|
2163
2471
|
}
|
|
2164
2472
|
/**
|
|
2165
|
-
*
|
|
2473
|
+
* Sign a user-level approval action (approve builder fee, approve agent).
|
|
2474
|
+
* These use the same EIP-712 structure but with different action payloads.
|
|
2166
2475
|
*/
|
|
2167
|
-
async
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2476
|
+
async signApproval(action, nonce) {
|
|
2477
|
+
return this.signAction(action, nonce);
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Get the signer's address
|
|
2481
|
+
*/
|
|
2482
|
+
getAddress() {
|
|
2483
|
+
const account = this.walletClient.account;
|
|
2484
|
+
if (!account) throw new Error("Wallet client has no account");
|
|
2485
|
+
return account.address;
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
function fillHashToBytes32(fillHash) {
|
|
2489
|
+
if (fillHash.startsWith("0x") && fillHash.length === 66) {
|
|
2490
|
+
return fillHash;
|
|
2491
|
+
}
|
|
2492
|
+
return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["string"], [fillHash]));
|
|
2493
|
+
}
|
|
2494
|
+
function fillOidToBytes32(oid) {
|
|
2495
|
+
return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["uint256"], [BigInt(oid)]));
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// src/perp/orders.ts
|
|
2499
|
+
var OrderManager = class {
|
|
2500
|
+
client;
|
|
2501
|
+
signer;
|
|
2502
|
+
config;
|
|
2503
|
+
constructor(client, signer, config) {
|
|
2504
|
+
this.client = client;
|
|
2505
|
+
this.signer = signer;
|
|
2506
|
+
this.config = config;
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Place an order on Hyperliquid from a trade signal.
|
|
2510
|
+
* Attaches builder fee for revenue collection.
|
|
2511
|
+
*/
|
|
2512
|
+
async placeOrder(signal) {
|
|
2513
|
+
try {
|
|
2514
|
+
const assetIndex = await this.client.getAssetIndex(signal.instrument);
|
|
2515
|
+
const isBuy = signal.action === "open_long" || signal.action === "close_short";
|
|
2516
|
+
const side = isBuy ? "B" : "A";
|
|
2517
|
+
const orderWire = {
|
|
2518
|
+
a: assetIndex,
|
|
2519
|
+
b: isBuy,
|
|
2520
|
+
p: signal.orderType === "market" ? this.getMarketPrice(signal) : signal.price.toString(),
|
|
2521
|
+
s: signal.size.toString(),
|
|
2522
|
+
r: signal.reduceOnly,
|
|
2523
|
+
t: signal.orderType === "market" ? { limit: { tif: "Ioc" } } : { limit: { tif: "Gtc" } }
|
|
2524
|
+
};
|
|
2525
|
+
const action = {
|
|
2526
|
+
type: "order",
|
|
2527
|
+
orders: [orderWire],
|
|
2528
|
+
grouping: "na",
|
|
2529
|
+
builder: {
|
|
2530
|
+
b: this.config.builderAddress,
|
|
2531
|
+
f: this.config.builderFeeTenthsBps
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
const nonce = getNextNonce();
|
|
2535
|
+
const { signature } = await this.signer.signAction(action, nonce);
|
|
2536
|
+
const address = this.signer.getAddress();
|
|
2537
|
+
const resp = await this.exchangeRequest({
|
|
2538
|
+
action,
|
|
2539
|
+
nonce: Number(nonce),
|
|
2540
|
+
signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
|
|
2541
|
+
vaultAddress: null
|
|
2542
|
+
});
|
|
2543
|
+
return this.parseOrderResponse(resp);
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2546
|
+
console.error(`Order placement failed for ${signal.instrument}:`, message);
|
|
2547
|
+
return {
|
|
2548
|
+
success: false,
|
|
2549
|
+
status: "error",
|
|
2550
|
+
error: message
|
|
2551
|
+
};
|
|
2173
2552
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Cancel an open order by ID.
|
|
2556
|
+
*/
|
|
2557
|
+
async cancelOrder(instrument, orderId) {
|
|
2558
|
+
try {
|
|
2559
|
+
const assetIndex = await this.client.getAssetIndex(instrument);
|
|
2560
|
+
const action = {
|
|
2561
|
+
type: "cancel",
|
|
2562
|
+
cancels: [{ a: assetIndex, o: orderId }]
|
|
2563
|
+
};
|
|
2564
|
+
const nonce = getNextNonce();
|
|
2565
|
+
const { signature } = await this.signer.signAction(action, nonce);
|
|
2566
|
+
await this.exchangeRequest({
|
|
2567
|
+
action,
|
|
2568
|
+
nonce: Number(nonce),
|
|
2569
|
+
signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
|
|
2570
|
+
vaultAddress: null
|
|
2571
|
+
});
|
|
2572
|
+
console.log(`Cancelled order ${orderId} for ${instrument}`);
|
|
2573
|
+
return true;
|
|
2574
|
+
} catch (error) {
|
|
2575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2576
|
+
console.error(`Cancel failed for order ${orderId}:`, message);
|
|
2577
|
+
return false;
|
|
2178
2578
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Close an entire position for an instrument.
|
|
2582
|
+
* Uses a market order with reduceOnly flag.
|
|
2583
|
+
*/
|
|
2584
|
+
async closePosition(instrument, positionSize) {
|
|
2585
|
+
const isLong = positionSize > 0;
|
|
2586
|
+
const signal = {
|
|
2587
|
+
action: isLong ? "close_long" : "close_short",
|
|
2588
|
+
instrument,
|
|
2589
|
+
size: Math.abs(positionSize),
|
|
2590
|
+
price: 0,
|
|
2591
|
+
leverage: 1,
|
|
2592
|
+
orderType: "market",
|
|
2593
|
+
reduceOnly: true,
|
|
2594
|
+
confidence: 1,
|
|
2595
|
+
reasoning: "Position close"
|
|
2596
|
+
};
|
|
2597
|
+
return this.placeOrder(signal);
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Update leverage for an instrument.
|
|
2601
|
+
*/
|
|
2602
|
+
async updateLeverage(instrument, leverage, isCross = true) {
|
|
2188
2603
|
try {
|
|
2189
|
-
await this.
|
|
2190
|
-
|
|
2191
|
-
|
|
2604
|
+
const assetIndex = await this.client.getAssetIndex(instrument);
|
|
2605
|
+
const action = {
|
|
2606
|
+
type: "updateLeverage",
|
|
2607
|
+
asset: assetIndex,
|
|
2608
|
+
isCross,
|
|
2609
|
+
leverage
|
|
2610
|
+
};
|
|
2611
|
+
const nonce = getNextNonce();
|
|
2612
|
+
const { signature } = await this.signer.signAction(action, nonce);
|
|
2613
|
+
await this.exchangeRequest({
|
|
2614
|
+
action,
|
|
2615
|
+
nonce: Number(nonce),
|
|
2616
|
+
signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
|
|
2617
|
+
vaultAddress: null
|
|
2618
|
+
});
|
|
2619
|
+
console.log(`Leverage updated for ${instrument}: ${leverage}x (${isCross ? "cross" : "isolated"})`);
|
|
2620
|
+
return true;
|
|
2192
2621
|
} catch (error) {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
);
|
|
2622
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2623
|
+
console.error(`Leverage update failed for ${instrument}:`, message);
|
|
2624
|
+
return false;
|
|
2197
2625
|
}
|
|
2198
2626
|
}
|
|
2627
|
+
// ============================================================
|
|
2628
|
+
// PRIVATE HELPERS
|
|
2629
|
+
// ============================================================
|
|
2199
2630
|
/**
|
|
2200
|
-
*
|
|
2631
|
+
* Get a market price string for IOC orders.
|
|
2632
|
+
* Uses a generous slippage buffer to ensure fills.
|
|
2201
2633
|
*/
|
|
2202
|
-
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2634
|
+
getMarketPrice(signal) {
|
|
2635
|
+
const isBuy = signal.action === "open_long" || signal.action === "close_short";
|
|
2636
|
+
if (signal.price > 0) {
|
|
2637
|
+
const slippage = isBuy ? 1.005 : 0.995;
|
|
2638
|
+
return (signal.price * slippage).toString();
|
|
2639
|
+
}
|
|
2640
|
+
return "0";
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Parse Hyperliquid exchange response into OrderResult.
|
|
2644
|
+
*/
|
|
2645
|
+
parseOrderResponse(resp) {
|
|
2646
|
+
if (resp?.status === "ok" && resp?.response?.type === "order") {
|
|
2647
|
+
const statuses = resp.response.data?.statuses || [];
|
|
2648
|
+
if (statuses.length > 0) {
|
|
2649
|
+
const status = statuses[0];
|
|
2650
|
+
if (status.filled) {
|
|
2651
|
+
return {
|
|
2652
|
+
success: true,
|
|
2653
|
+
orderId: status.filled.oid,
|
|
2654
|
+
status: "filled",
|
|
2655
|
+
avgPrice: status.filled.avgPx,
|
|
2656
|
+
filledSize: status.filled.totalSz
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
if (status.resting) {
|
|
2660
|
+
return {
|
|
2661
|
+
success: true,
|
|
2662
|
+
orderId: status.resting.oid,
|
|
2663
|
+
status: "resting"
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
if (status.error) {
|
|
2667
|
+
return {
|
|
2668
|
+
success: false,
|
|
2669
|
+
status: "error",
|
|
2670
|
+
error: status.error
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2220
2673
|
}
|
|
2221
2674
|
}
|
|
2675
|
+
return {
|
|
2676
|
+
success: false,
|
|
2677
|
+
status: "error",
|
|
2678
|
+
error: `Unexpected response: ${JSON.stringify(resp)}`
|
|
2679
|
+
};
|
|
2222
2680
|
}
|
|
2223
2681
|
/**
|
|
2224
|
-
*
|
|
2225
|
-
* If the trading wallet differs from the owner, enters a recovery loop
|
|
2226
|
-
* that waits for the owner to link it from the website.
|
|
2682
|
+
* Send a signed request to the Hyperliquid Exchange API.
|
|
2227
2683
|
*/
|
|
2228
|
-
async
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2684
|
+
async exchangeRequest(body) {
|
|
2685
|
+
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
2686
|
+
method: "POST",
|
|
2687
|
+
headers: { "Content-Type": "application/json" },
|
|
2688
|
+
body: JSON.stringify(body)
|
|
2689
|
+
});
|
|
2690
|
+
if (!resp.ok) {
|
|
2691
|
+
throw new Error(`Hyperliquid Exchange API error: ${resp.status} ${await resp.text()}`);
|
|
2692
|
+
}
|
|
2693
|
+
return resp.json();
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
|
|
2697
|
+
// src/perp/positions.ts
|
|
2698
|
+
var PositionManager = class {
|
|
2699
|
+
client;
|
|
2700
|
+
userAddress;
|
|
2701
|
+
/** Cached positions (updated each cycle) */
|
|
2702
|
+
cachedPositions = [];
|
|
2703
|
+
cachedAccount = null;
|
|
2704
|
+
lastRefreshAt = 0;
|
|
2705
|
+
/** Cache TTL in ms (5 seconds — positions refresh each cycle anyway) */
|
|
2706
|
+
cacheTtlMs = 5e3;
|
|
2707
|
+
constructor(client, userAddress) {
|
|
2708
|
+
this.client = client;
|
|
2709
|
+
this.userAddress = userAddress;
|
|
2710
|
+
}
|
|
2711
|
+
// ============================================================
|
|
2712
|
+
// POSITION QUERIES
|
|
2713
|
+
// ============================================================
|
|
2714
|
+
/**
|
|
2715
|
+
* Get all open positions. Uses cache if fresh.
|
|
2716
|
+
*/
|
|
2717
|
+
async getPositions(forceRefresh = false) {
|
|
2718
|
+
if (!forceRefresh && this.isCacheFresh()) {
|
|
2719
|
+
return this.cachedPositions;
|
|
2720
|
+
}
|
|
2721
|
+
await this.refresh();
|
|
2722
|
+
return this.cachedPositions;
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Get a specific position by instrument.
|
|
2726
|
+
* Returns null if no position is open.
|
|
2727
|
+
*/
|
|
2728
|
+
async getPosition(instrument) {
|
|
2729
|
+
const positions = await this.getPositions();
|
|
2730
|
+
return positions.find((p) => p.instrument === instrument) ?? null;
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Get account summary (equity, margin, leverage).
|
|
2734
|
+
*/
|
|
2735
|
+
async getAccountSummary(forceRefresh = false) {
|
|
2736
|
+
if (!forceRefresh && this.isCacheFresh() && this.cachedAccount) {
|
|
2737
|
+
return this.cachedAccount;
|
|
2738
|
+
}
|
|
2739
|
+
await this.refresh();
|
|
2740
|
+
return this.cachedAccount;
|
|
2741
|
+
}
|
|
2742
|
+
// ============================================================
|
|
2743
|
+
// LIQUIDATION MONITORING
|
|
2744
|
+
// ============================================================
|
|
2745
|
+
/**
|
|
2746
|
+
* Get liquidation proximity for all positions.
|
|
2747
|
+
* Returns a value between 0.0 (safe) and 1.0 (at liquidation price).
|
|
2748
|
+
* Values above 0.7 should trigger risk warnings.
|
|
2749
|
+
*/
|
|
2750
|
+
async getLiquidationProximity() {
|
|
2751
|
+
const positions = await this.getPositions();
|
|
2752
|
+
const proximities = /* @__PURE__ */ new Map();
|
|
2753
|
+
for (const pos of positions) {
|
|
2754
|
+
if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) {
|
|
2755
|
+
proximities.set(pos.instrument, 0);
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
let proximity;
|
|
2759
|
+
if (pos.size > 0) {
|
|
2760
|
+
if (pos.markPrice <= pos.liquidationPrice) {
|
|
2761
|
+
proximity = 1;
|
|
2762
|
+
} else {
|
|
2763
|
+
const distanceToLiq = pos.markPrice - pos.liquidationPrice;
|
|
2764
|
+
const entryToLiq = pos.entryPrice - pos.liquidationPrice;
|
|
2765
|
+
proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
|
|
2766
|
+
}
|
|
2767
|
+
} else {
|
|
2768
|
+
if (pos.markPrice >= pos.liquidationPrice) {
|
|
2769
|
+
proximity = 1;
|
|
2770
|
+
} else {
|
|
2771
|
+
const distanceToLiq = pos.liquidationPrice - pos.markPrice;
|
|
2772
|
+
const entryToLiq = pos.liquidationPrice - pos.entryPrice;
|
|
2773
|
+
proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
proximities.set(pos.instrument, Math.max(0, Math.min(1, proximity)));
|
|
2777
|
+
}
|
|
2778
|
+
return proximities;
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Check if any position is dangerously close to liquidation.
|
|
2782
|
+
* Returns instruments with proximity > threshold.
|
|
2783
|
+
*/
|
|
2784
|
+
async getDangerousPositions(threshold = 0.7) {
|
|
2785
|
+
const positions = await this.getPositions();
|
|
2786
|
+
const proximities = await this.getLiquidationProximity();
|
|
2787
|
+
return positions.filter((p) => {
|
|
2788
|
+
const prox = proximities.get(p.instrument) ?? 0;
|
|
2789
|
+
return prox > threshold;
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
// ============================================================
|
|
2793
|
+
// SUMMARY HELPERS
|
|
2794
|
+
// ============================================================
|
|
2795
|
+
/**
|
|
2796
|
+
* Get total unrealized PnL across all positions.
|
|
2797
|
+
*/
|
|
2798
|
+
async getTotalUnrealizedPnl() {
|
|
2799
|
+
const positions = await this.getPositions();
|
|
2800
|
+
return positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Get total notional exposure.
|
|
2804
|
+
*/
|
|
2805
|
+
async getTotalNotional() {
|
|
2806
|
+
const positions = await this.getPositions();
|
|
2807
|
+
return positions.reduce((sum, p) => sum + p.notionalUSD, 0);
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Get position count.
|
|
2811
|
+
*/
|
|
2812
|
+
async getPositionCount() {
|
|
2813
|
+
const positions = await this.getPositions();
|
|
2814
|
+
return positions.length;
|
|
2815
|
+
}
|
|
2816
|
+
// ============================================================
|
|
2817
|
+
// CACHE MANAGEMENT
|
|
2818
|
+
// ============================================================
|
|
2819
|
+
/**
|
|
2820
|
+
* Force refresh positions and account from Hyperliquid.
|
|
2821
|
+
*/
|
|
2822
|
+
async refresh() {
|
|
2823
|
+
try {
|
|
2824
|
+
const [positions, account] = await Promise.all([
|
|
2825
|
+
this.client.getPositions(this.userAddress),
|
|
2826
|
+
this.client.getAccountSummary(this.userAddress)
|
|
2827
|
+
]);
|
|
2828
|
+
this.cachedPositions = positions;
|
|
2829
|
+
this.cachedAccount = account;
|
|
2830
|
+
this.lastRefreshAt = Date.now();
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2833
|
+
console.error("Failed to refresh positions:", message);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Check if cache is still fresh.
|
|
2838
|
+
*/
|
|
2839
|
+
isCacheFresh() {
|
|
2840
|
+
return Date.now() - this.lastRefreshAt < this.cacheTtlMs;
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
|
|
2844
|
+
// src/perp/websocket.ts
|
|
2845
|
+
var import_ws2 = __toESM(require("ws"));
|
|
2846
|
+
var HyperliquidWebSocket = class {
|
|
2847
|
+
wsUrl;
|
|
2848
|
+
userAddress;
|
|
2849
|
+
client;
|
|
2850
|
+
ws = null;
|
|
2851
|
+
reconnectAttempts = 0;
|
|
2852
|
+
maxReconnectAttempts = 20;
|
|
2853
|
+
baseReconnectMs = 1e3;
|
|
2854
|
+
maxReconnectMs = 6e4;
|
|
2855
|
+
reconnectTimer = null;
|
|
2856
|
+
pingTimer = null;
|
|
2857
|
+
isConnecting = false;
|
|
2858
|
+
shouldReconnect = true;
|
|
2859
|
+
/** Last processed fill time (ms) — used for REST backfill on reconnect */
|
|
2860
|
+
lastProcessedFillTime = 0;
|
|
2861
|
+
/** Callbacks */
|
|
2862
|
+
onFill = null;
|
|
2863
|
+
onFunding = null;
|
|
2864
|
+
onLiquidation = null;
|
|
2865
|
+
constructor(config, userAddress, client) {
|
|
2866
|
+
this.wsUrl = config.wsUrl;
|
|
2867
|
+
this.userAddress = userAddress;
|
|
2868
|
+
this.client = client;
|
|
2869
|
+
}
|
|
2870
|
+
// ============================================================
|
|
2871
|
+
// CONNECTION
|
|
2872
|
+
// ============================================================
|
|
2873
|
+
/**
|
|
2874
|
+
* Connect to Hyperliquid WebSocket and subscribe to user events.
|
|
2875
|
+
*/
|
|
2876
|
+
async connect() {
|
|
2877
|
+
if (this.ws?.readyState === import_ws2.default.OPEN || this.isConnecting) {
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
this.isConnecting = true;
|
|
2881
|
+
this.shouldReconnect = true;
|
|
2882
|
+
return new Promise((resolve, reject) => {
|
|
2883
|
+
try {
|
|
2884
|
+
this.ws = new import_ws2.default(this.wsUrl);
|
|
2885
|
+
this.ws.on("open", () => {
|
|
2886
|
+
this.isConnecting = false;
|
|
2887
|
+
this.reconnectAttempts = 0;
|
|
2888
|
+
console.log("Hyperliquid WebSocket connected");
|
|
2889
|
+
this.subscribe({
|
|
2890
|
+
type: "subscribe",
|
|
2891
|
+
subscription: { type: "userFills", user: this.userAddress }
|
|
2892
|
+
});
|
|
2893
|
+
this.subscribe({
|
|
2894
|
+
type: "subscribe",
|
|
2895
|
+
subscription: { type: "userFundings", user: this.userAddress }
|
|
2896
|
+
});
|
|
2897
|
+
this.startPing();
|
|
2898
|
+
this.backfillMissedFills().catch((err) => {
|
|
2899
|
+
console.warn("Fill backfill failed:", err instanceof Error ? err.message : err);
|
|
2900
|
+
});
|
|
2901
|
+
resolve();
|
|
2902
|
+
});
|
|
2903
|
+
this.ws.on("message", (data) => {
|
|
2904
|
+
this.handleMessage(data);
|
|
2905
|
+
});
|
|
2906
|
+
this.ws.on("close", (code, reason) => {
|
|
2907
|
+
this.isConnecting = false;
|
|
2908
|
+
console.log(`Hyperliquid WebSocket closed: ${code} ${reason.toString()}`);
|
|
2909
|
+
this.stopPing();
|
|
2910
|
+
this.scheduleReconnect();
|
|
2911
|
+
});
|
|
2912
|
+
this.ws.on("error", (error) => {
|
|
2913
|
+
this.isConnecting = false;
|
|
2914
|
+
console.error("Hyperliquid WebSocket error:", error.message);
|
|
2915
|
+
if (this.reconnectAttempts === 0) {
|
|
2916
|
+
reject(error);
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
} catch (error) {
|
|
2920
|
+
this.isConnecting = false;
|
|
2921
|
+
reject(error);
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Disconnect and stop reconnecting.
|
|
2927
|
+
*/
|
|
2928
|
+
disconnect() {
|
|
2929
|
+
this.shouldReconnect = false;
|
|
2930
|
+
if (this.reconnectTimer) {
|
|
2931
|
+
clearTimeout(this.reconnectTimer);
|
|
2932
|
+
this.reconnectTimer = null;
|
|
2933
|
+
}
|
|
2934
|
+
this.stopPing();
|
|
2935
|
+
if (this.ws) {
|
|
2936
|
+
this.ws.removeAllListeners();
|
|
2937
|
+
if (this.ws.readyState === import_ws2.default.OPEN) {
|
|
2938
|
+
this.ws.close(1e3, "Client disconnect");
|
|
2939
|
+
}
|
|
2940
|
+
this.ws = null;
|
|
2941
|
+
}
|
|
2942
|
+
console.log("Hyperliquid WebSocket disconnected");
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Check if WebSocket is connected.
|
|
2946
|
+
*/
|
|
2947
|
+
get isConnected() {
|
|
2948
|
+
return this.ws?.readyState === import_ws2.default.OPEN;
|
|
2949
|
+
}
|
|
2950
|
+
// ============================================================
|
|
2951
|
+
// EVENT HANDLERS
|
|
2952
|
+
// ============================================================
|
|
2953
|
+
/**
|
|
2954
|
+
* Register callback for fill events.
|
|
2955
|
+
*/
|
|
2956
|
+
onFillReceived(callback) {
|
|
2957
|
+
this.onFill = callback;
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Register callback for funding payment events.
|
|
2961
|
+
*/
|
|
2962
|
+
onFundingReceived(callback) {
|
|
2963
|
+
this.onFunding = callback;
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Register callback for liquidation events.
|
|
2967
|
+
*/
|
|
2968
|
+
onLiquidationDetected(callback) {
|
|
2969
|
+
this.onLiquidation = callback;
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Get the last processed fill time (for external checkpoint management).
|
|
2973
|
+
*/
|
|
2974
|
+
getLastProcessedFillTime() {
|
|
2975
|
+
return this.lastProcessedFillTime;
|
|
2976
|
+
}
|
|
2977
|
+
// ============================================================
|
|
2978
|
+
// MESSAGE HANDLING
|
|
2979
|
+
// ============================================================
|
|
2980
|
+
handleMessage(data) {
|
|
2981
|
+
try {
|
|
2982
|
+
const msg = JSON.parse(data.toString());
|
|
2983
|
+
if (msg.channel === "userFills") {
|
|
2984
|
+
this.handleFillMessage(msg.data);
|
|
2985
|
+
} else if (msg.channel === "userFundings") {
|
|
2986
|
+
this.handleFundingMessage(msg.data);
|
|
2987
|
+
}
|
|
2988
|
+
} catch (error) {
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
handleFillMessage(fills) {
|
|
2992
|
+
if (!Array.isArray(fills) || !this.onFill) return;
|
|
2993
|
+
for (const rawFill of fills) {
|
|
2994
|
+
const fill = this.client.parseFill(rawFill);
|
|
2995
|
+
if (fill.time > this.lastProcessedFillTime) {
|
|
2996
|
+
this.lastProcessedFillTime = fill.time;
|
|
2997
|
+
}
|
|
2998
|
+
if (fill.liquidation && this.onLiquidation) {
|
|
2999
|
+
this.onLiquidation(fill.coin, parseFloat(fill.sz));
|
|
3000
|
+
}
|
|
3001
|
+
this.onFill(fill);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
handleFundingMessage(fundings) {
|
|
3005
|
+
if (!Array.isArray(fundings) || !this.onFunding) return;
|
|
3006
|
+
for (const funding of fundings) {
|
|
3007
|
+
this.onFunding({
|
|
3008
|
+
time: funding.time,
|
|
3009
|
+
coin: funding.coin,
|
|
3010
|
+
usdc: funding.usdc,
|
|
3011
|
+
szi: funding.szi,
|
|
3012
|
+
fundingRate: funding.fundingRate
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
// ============================================================
|
|
3017
|
+
// BACKFILL
|
|
3018
|
+
// ============================================================
|
|
3019
|
+
/**
|
|
3020
|
+
* Backfill fills that may have been missed during WebSocket downtime.
|
|
3021
|
+
* Uses the last processed fill time as the starting point.
|
|
3022
|
+
*/
|
|
3023
|
+
async backfillMissedFills() {
|
|
3024
|
+
if (this.lastProcessedFillTime === 0 || !this.onFill) {
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
console.log(`Backfilling fills since ${new Date(this.lastProcessedFillTime).toISOString()}`);
|
|
3028
|
+
const fills = await this.client.getUserFillsByTime(
|
|
3029
|
+
this.userAddress,
|
|
3030
|
+
this.lastProcessedFillTime + 1
|
|
3031
|
+
// +1 to avoid duplicate
|
|
3032
|
+
);
|
|
3033
|
+
if (fills.length > 0) {
|
|
3034
|
+
console.log(`Backfilled ${fills.length} missed fills`);
|
|
3035
|
+
for (const rawFill of fills) {
|
|
3036
|
+
const fill = this.client.parseFill(rawFill);
|
|
3037
|
+
if (fill.time > this.lastProcessedFillTime) {
|
|
3038
|
+
this.lastProcessedFillTime = fill.time;
|
|
3039
|
+
}
|
|
3040
|
+
if (fill.liquidation && this.onLiquidation) {
|
|
3041
|
+
this.onLiquidation(fill.coin, parseFloat(fill.sz));
|
|
3042
|
+
}
|
|
3043
|
+
this.onFill(fill);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
// ============================================================
|
|
3048
|
+
// RECONNECTION
|
|
3049
|
+
// ============================================================
|
|
3050
|
+
scheduleReconnect() {
|
|
3051
|
+
if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
3052
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
3053
|
+
console.error(`Hyperliquid WebSocket: max reconnect attempts (${this.maxReconnectAttempts}) reached`);
|
|
3054
|
+
}
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
const delay = Math.min(
|
|
3058
|
+
this.baseReconnectMs * Math.pow(2, this.reconnectAttempts),
|
|
3059
|
+
this.maxReconnectMs
|
|
3060
|
+
);
|
|
3061
|
+
this.reconnectAttempts++;
|
|
3062
|
+
console.log(`Hyperliquid WebSocket: reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
3063
|
+
this.reconnectTimer = setTimeout(() => {
|
|
3064
|
+
this.connect().catch((err) => {
|
|
3065
|
+
console.error("Reconnect failed:", err instanceof Error ? err.message : err);
|
|
3066
|
+
});
|
|
3067
|
+
}, delay);
|
|
3068
|
+
}
|
|
3069
|
+
// ============================================================
|
|
3070
|
+
// KEEPALIVE
|
|
3071
|
+
// ============================================================
|
|
3072
|
+
startPing() {
|
|
3073
|
+
this.stopPing();
|
|
3074
|
+
this.pingTimer = setInterval(() => {
|
|
3075
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
3076
|
+
this.ws.send(JSON.stringify({ method: "ping" }));
|
|
3077
|
+
}
|
|
3078
|
+
}, 25e3);
|
|
3079
|
+
}
|
|
3080
|
+
stopPing() {
|
|
3081
|
+
if (this.pingTimer) {
|
|
3082
|
+
clearInterval(this.pingTimer);
|
|
3083
|
+
this.pingTimer = null;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
// ============================================================
|
|
3087
|
+
// HELPERS
|
|
3088
|
+
// ============================================================
|
|
3089
|
+
subscribe(msg) {
|
|
3090
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
3091
|
+
this.ws.send(JSON.stringify(msg));
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
|
|
3096
|
+
// src/perp/recorder.ts
|
|
3097
|
+
var import_viem4 = require("viem");
|
|
3098
|
+
var import_chains2 = require("viem/chains");
|
|
3099
|
+
var import_accounts3 = require("viem/accounts");
|
|
3100
|
+
var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
|
|
3101
|
+
var ROUTER_ABI = [
|
|
3102
|
+
{
|
|
3103
|
+
type: "function",
|
|
3104
|
+
name: "recordPerpTrade",
|
|
3105
|
+
stateMutability: "nonpayable",
|
|
3106
|
+
inputs: [
|
|
3107
|
+
{ name: "agentId", type: "uint256" },
|
|
3108
|
+
{ name: "configHash", type: "bytes32" },
|
|
3109
|
+
{ name: "instrument", type: "string" },
|
|
3110
|
+
{ name: "isLong", type: "bool" },
|
|
3111
|
+
{ name: "notionalUSD", type: "uint256" },
|
|
3112
|
+
{ name: "feeUSD", type: "uint256" },
|
|
3113
|
+
{ name: "fillId", type: "bytes32" }
|
|
3114
|
+
],
|
|
3115
|
+
outputs: [{ name: "", type: "bool" }]
|
|
3116
|
+
}
|
|
3117
|
+
];
|
|
3118
|
+
var MAX_RETRIES = 3;
|
|
3119
|
+
var RETRY_DELAY_MS = 5e3;
|
|
3120
|
+
var PerpTradeRecorder = class {
|
|
3121
|
+
// Use `any` for viem client types to avoid L2 chain type conflicts (Base has "deposit" tx type)
|
|
3122
|
+
publicClient;
|
|
3123
|
+
// eslint-disable-line @typescript-eslint/no-explicit-any
|
|
3124
|
+
walletClient;
|
|
3125
|
+
// eslint-disable-line @typescript-eslint/no-explicit-any
|
|
3126
|
+
account;
|
|
3127
|
+
agentId;
|
|
3128
|
+
configHash;
|
|
3129
|
+
/** Retry queue for failed recordings */
|
|
3130
|
+
retryQueue = [];
|
|
3131
|
+
/** Set of fill IDs already recorded (or in-progress) to prevent local dups */
|
|
3132
|
+
recordedFills = /* @__PURE__ */ new Set();
|
|
3133
|
+
/** Timer for processing retry queue */
|
|
3134
|
+
retryTimer = null;
|
|
3135
|
+
constructor(opts) {
|
|
3136
|
+
this.agentId = opts.agentId;
|
|
3137
|
+
this.configHash = opts.configHash;
|
|
3138
|
+
this.account = (0, import_accounts3.privateKeyToAccount)(opts.privateKey);
|
|
3139
|
+
const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
|
|
3140
|
+
const transport = (0, import_viem4.http)(rpcUrl);
|
|
3141
|
+
this.publicClient = (0, import_viem4.createPublicClient)({
|
|
3142
|
+
chain: import_chains2.base,
|
|
3143
|
+
transport
|
|
3144
|
+
});
|
|
3145
|
+
this.walletClient = (0, import_viem4.createWalletClient)({
|
|
3146
|
+
chain: import_chains2.base,
|
|
3147
|
+
transport,
|
|
3148
|
+
account: this.account
|
|
3149
|
+
});
|
|
3150
|
+
this.retryTimer = setInterval(() => this.processRetryQueue(), RETRY_DELAY_MS);
|
|
3151
|
+
}
|
|
3152
|
+
// ============================================================
|
|
3153
|
+
// PUBLIC API
|
|
3154
|
+
// ============================================================
|
|
3155
|
+
/**
|
|
3156
|
+
* Record a fill on-chain.
|
|
3157
|
+
* Converts the Hyperliquid fill into recordPerpTrade params and submits.
|
|
3158
|
+
*/
|
|
3159
|
+
async recordFill(fill) {
|
|
3160
|
+
const fillId = fillHashToBytes32(fill.hash);
|
|
3161
|
+
const fillIdStr = fillId.toLowerCase();
|
|
3162
|
+
if (this.recordedFills.has(fillIdStr)) {
|
|
3163
|
+
return { success: true };
|
|
3164
|
+
}
|
|
3165
|
+
this.recordedFills.add(fillIdStr);
|
|
3166
|
+
const params = {
|
|
3167
|
+
agentId: this.agentId,
|
|
3168
|
+
configHash: this.configHash,
|
|
3169
|
+
instrument: fill.coin,
|
|
3170
|
+
isLong: fill.side === "B",
|
|
3171
|
+
notionalUSD: this.calculateNotionalUSD(fill),
|
|
3172
|
+
feeUSD: this.calculateFeeUSD(fill),
|
|
3173
|
+
fillId
|
|
3174
|
+
};
|
|
3175
|
+
return this.submitRecord(params);
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Update the config hash (when epoch changes).
|
|
3179
|
+
*/
|
|
3180
|
+
updateConfigHash(configHash) {
|
|
3181
|
+
this.configHash = configHash;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Get the number of fills pending retry.
|
|
3185
|
+
*/
|
|
3186
|
+
get pendingRetries() {
|
|
3187
|
+
return this.retryQueue.length;
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Get the number of fills recorded (local dedup set size).
|
|
3191
|
+
*/
|
|
3192
|
+
get recordedCount() {
|
|
3193
|
+
return this.recordedFills.size;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Stop the recorder (clear retry timer).
|
|
3197
|
+
*/
|
|
3198
|
+
stop() {
|
|
3199
|
+
if (this.retryTimer) {
|
|
3200
|
+
clearInterval(this.retryTimer);
|
|
3201
|
+
this.retryTimer = null;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
// ============================================================
|
|
3205
|
+
// PRIVATE
|
|
3206
|
+
// ============================================================
|
|
3207
|
+
/**
|
|
3208
|
+
* Submit a recordPerpTrade transaction on Base.
|
|
3209
|
+
*/
|
|
3210
|
+
async submitRecord(params) {
|
|
3211
|
+
try {
|
|
3212
|
+
const { request } = await this.publicClient.simulateContract({
|
|
3213
|
+
address: ROUTER_ADDRESS,
|
|
3214
|
+
abi: ROUTER_ABI,
|
|
3215
|
+
functionName: "recordPerpTrade",
|
|
3216
|
+
args: [
|
|
3217
|
+
params.agentId,
|
|
3218
|
+
params.configHash,
|
|
3219
|
+
params.instrument,
|
|
3220
|
+
params.isLong,
|
|
3221
|
+
params.notionalUSD,
|
|
3222
|
+
params.feeUSD,
|
|
3223
|
+
params.fillId
|
|
3224
|
+
],
|
|
3225
|
+
account: this.account
|
|
3226
|
+
});
|
|
3227
|
+
const txHash = await this.walletClient.writeContract(request);
|
|
3228
|
+
console.log(`Perp trade recorded: ${params.instrument} ${params.isLong ? "LONG" : "SHORT"} $${Number(params.notionalUSD) / 1e6} \u2014 tx: ${txHash}`);
|
|
3229
|
+
return { success: true, txHash };
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3232
|
+
if (message.includes("DuplicateFill") || message.includes("already recorded")) {
|
|
3233
|
+
console.log(`Fill already recorded on-chain: ${params.fillId}`);
|
|
3234
|
+
return { success: true };
|
|
3235
|
+
}
|
|
3236
|
+
console.error(`Failed to record perp trade: ${message}`);
|
|
3237
|
+
this.retryQueue.push({
|
|
3238
|
+
params,
|
|
3239
|
+
retries: 0,
|
|
3240
|
+
lastAttempt: Date.now()
|
|
3241
|
+
});
|
|
3242
|
+
return { success: false, error: message };
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Process the retry queue — attempt to re-submit failed recordings.
|
|
3247
|
+
*/
|
|
3248
|
+
async processRetryQueue() {
|
|
3249
|
+
if (this.retryQueue.length === 0) return;
|
|
3250
|
+
const now = Date.now();
|
|
3251
|
+
const toRetry = this.retryQueue.filter(
|
|
3252
|
+
(item) => now - item.lastAttempt >= RETRY_DELAY_MS
|
|
3253
|
+
);
|
|
3254
|
+
for (const item of toRetry) {
|
|
3255
|
+
item.retries++;
|
|
3256
|
+
item.lastAttempt = now;
|
|
3257
|
+
if (item.retries > MAX_RETRIES) {
|
|
3258
|
+
console.error(
|
|
3259
|
+
`Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
|
|
3260
|
+
);
|
|
3261
|
+
const idx = this.retryQueue.indexOf(item);
|
|
3262
|
+
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
3265
|
+
console.log(
|
|
3266
|
+
`Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
|
|
3267
|
+
);
|
|
3268
|
+
const result = await this.submitRecord(item.params);
|
|
3269
|
+
if (result.success) {
|
|
3270
|
+
const idx = this.retryQueue.indexOf(item);
|
|
3271
|
+
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
// ============================================================
|
|
3276
|
+
// CONVERSION HELPERS
|
|
3277
|
+
// ============================================================
|
|
3278
|
+
/**
|
|
3279
|
+
* Calculate notional USD from a fill (6-decimal).
|
|
3280
|
+
* notionalUSD = px * sz * 1e6
|
|
3281
|
+
*/
|
|
3282
|
+
calculateNotionalUSD(fill) {
|
|
3283
|
+
const px = parseFloat(fill.px);
|
|
3284
|
+
const sz = parseFloat(fill.sz);
|
|
3285
|
+
return BigInt(Math.round(px * sz * 1e6));
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Calculate fee USD from a fill (6-decimal).
|
|
3289
|
+
* feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
|
|
3290
|
+
*/
|
|
3291
|
+
calculateFeeUSD(fill) {
|
|
3292
|
+
const fee = parseFloat(fill.fee);
|
|
3293
|
+
const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
|
|
3294
|
+
return BigInt(Math.round((fee + builderFee) * 1e6));
|
|
3295
|
+
}
|
|
3296
|
+
};
|
|
3297
|
+
|
|
3298
|
+
// src/perp/onboarding.ts
|
|
3299
|
+
var PerpOnboarding = class {
|
|
3300
|
+
client;
|
|
3301
|
+
signer;
|
|
3302
|
+
config;
|
|
3303
|
+
constructor(client, signer, config) {
|
|
3304
|
+
this.client = client;
|
|
3305
|
+
this.signer = signer;
|
|
3306
|
+
this.config = config;
|
|
3307
|
+
}
|
|
3308
|
+
// ============================================================
|
|
3309
|
+
// BUILDER FEE
|
|
3310
|
+
// ============================================================
|
|
3311
|
+
/**
|
|
3312
|
+
* Check if the user has approved the builder fee.
|
|
3313
|
+
* Builder fee must be approved before orders can include builder fees.
|
|
3314
|
+
*/
|
|
3315
|
+
async isBuilderFeeApproved() {
|
|
3316
|
+
try {
|
|
3317
|
+
const maxFee = await this.client.getMaxBuilderFee(
|
|
3318
|
+
this.signer.getAddress(),
|
|
3319
|
+
this.config.builderAddress
|
|
3320
|
+
);
|
|
3321
|
+
return maxFee >= this.config.builderFeeTenthsBps;
|
|
3322
|
+
} catch {
|
|
3323
|
+
return false;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Approve the builder fee on Hyperliquid.
|
|
3328
|
+
* This is a one-time approval per builder address.
|
|
3329
|
+
*/
|
|
3330
|
+
async approveBuilderFee() {
|
|
3331
|
+
try {
|
|
3332
|
+
const action = {
|
|
3333
|
+
type: "approveBuilderFee",
|
|
3334
|
+
hyperliquidChain: "Mainnet",
|
|
3335
|
+
maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
|
|
3336
|
+
builder: this.config.builderAddress,
|
|
3337
|
+
nonce: Number(getNextNonce())
|
|
3338
|
+
};
|
|
3339
|
+
const { signature } = await this.signer.signApproval(action);
|
|
3340
|
+
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
3341
|
+
method: "POST",
|
|
3342
|
+
headers: { "Content-Type": "application/json" },
|
|
3343
|
+
body: JSON.stringify({
|
|
3344
|
+
action,
|
|
3345
|
+
signature: {
|
|
3346
|
+
r: signature.slice(0, 66),
|
|
3347
|
+
s: `0x${signature.slice(66, 130)}`,
|
|
3348
|
+
v: parseInt(signature.slice(130, 132), 16)
|
|
3349
|
+
},
|
|
3350
|
+
nonce: action.nonce,
|
|
3351
|
+
vaultAddress: null
|
|
3352
|
+
})
|
|
3353
|
+
});
|
|
3354
|
+
if (!resp.ok) {
|
|
3355
|
+
const text = await resp.text();
|
|
3356
|
+
console.error(`Builder fee approval failed: ${resp.status} ${text}`);
|
|
3357
|
+
return false;
|
|
3358
|
+
}
|
|
3359
|
+
console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
|
|
3360
|
+
return true;
|
|
3361
|
+
} catch (error) {
|
|
3362
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3363
|
+
console.error(`Builder fee approval failed: ${message}`);
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
// ============================================================
|
|
3368
|
+
// BALANCE & REQUIREMENTS
|
|
3369
|
+
// ============================================================
|
|
3370
|
+
/**
|
|
3371
|
+
* Check if the user has sufficient USDC balance on Hyperliquid.
|
|
3372
|
+
* Returns the account equity in USD.
|
|
3373
|
+
*/
|
|
3374
|
+
async checkBalance() {
|
|
3375
|
+
try {
|
|
3376
|
+
const account = await this.client.getAccountSummary(this.signer.getAddress());
|
|
3377
|
+
return {
|
|
3378
|
+
hasBalance: account.totalEquity > 0,
|
|
3379
|
+
equity: account.totalEquity
|
|
3380
|
+
};
|
|
3381
|
+
} catch {
|
|
3382
|
+
return { hasBalance: false, equity: 0 };
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Verify that the agent's risk universe allows perp trading.
|
|
3387
|
+
* Perps require risk universe >= 2 (Derivatives or higher).
|
|
3388
|
+
*/
|
|
3389
|
+
verifyRiskUniverse(riskUniverse) {
|
|
3390
|
+
if (riskUniverse >= 2) {
|
|
3391
|
+
return {
|
|
3392
|
+
allowed: true,
|
|
3393
|
+
message: `Risk universe ${riskUniverse} allows perp trading`
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
return {
|
|
3397
|
+
allowed: false,
|
|
3398
|
+
message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
// ============================================================
|
|
3402
|
+
// FULL ONBOARDING CHECK
|
|
3403
|
+
// ============================================================
|
|
3404
|
+
/**
|
|
3405
|
+
* Run all onboarding checks and return status.
|
|
3406
|
+
* Does NOT auto-approve — caller must explicitly approve after review.
|
|
3407
|
+
*/
|
|
3408
|
+
async checkOnboardingStatus(riskUniverse) {
|
|
3409
|
+
const riskCheck = this.verifyRiskUniverse(riskUniverse);
|
|
3410
|
+
const balanceCheck = await this.checkBalance();
|
|
3411
|
+
const builderFeeApproved = await this.isBuilderFeeApproved();
|
|
3412
|
+
const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
|
|
3413
|
+
return {
|
|
3414
|
+
ready,
|
|
3415
|
+
riskUniverseOk: riskCheck.allowed,
|
|
3416
|
+
riskUniverseMessage: riskCheck.message,
|
|
3417
|
+
hasBalance: balanceCheck.hasBalance,
|
|
3418
|
+
equity: balanceCheck.equity,
|
|
3419
|
+
builderFeeApproved,
|
|
3420
|
+
builderAddress: this.config.builderAddress,
|
|
3421
|
+
builderFeeBps: this.config.builderFeeTenthsBps / 10
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Run full onboarding: check status and auto-approve builder fee if needed.
|
|
3426
|
+
* Returns the final status after all actions.
|
|
3427
|
+
*/
|
|
3428
|
+
async onboard(riskUniverse) {
|
|
3429
|
+
let status = await this.checkOnboardingStatus(riskUniverse);
|
|
3430
|
+
if (!status.riskUniverseOk) {
|
|
3431
|
+
console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
|
|
3432
|
+
return status;
|
|
3433
|
+
}
|
|
3434
|
+
if (!status.hasBalance) {
|
|
3435
|
+
console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
|
|
3436
|
+
return status;
|
|
3437
|
+
}
|
|
3438
|
+
if (!status.builderFeeApproved) {
|
|
3439
|
+
console.log("Approving builder fee...");
|
|
3440
|
+
const approved = await this.approveBuilderFee();
|
|
3441
|
+
if (approved) {
|
|
3442
|
+
status = { ...status, builderFeeApproved: true, ready: true };
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
if (status.ready) {
|
|
3446
|
+
console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
|
|
3447
|
+
}
|
|
3448
|
+
return status;
|
|
3449
|
+
}
|
|
3450
|
+
};
|
|
3451
|
+
|
|
3452
|
+
// src/perp/funding.ts
|
|
3453
|
+
var import_viem5 = require("viem");
|
|
3454
|
+
var import_chains3 = require("viem/chains");
|
|
3455
|
+
var import_accounts4 = require("viem/accounts");
|
|
3456
|
+
var ERC20_ABI = (0, import_viem5.parseAbi)([
|
|
3457
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
3458
|
+
"function balanceOf(address account) external view returns (uint256)",
|
|
3459
|
+
"function allowance(address owner, address spender) external view returns (uint256)"
|
|
3460
|
+
]);
|
|
3461
|
+
var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
|
|
3462
|
+
"function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
|
|
3463
|
+
"event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
|
|
3464
|
+
]);
|
|
3465
|
+
var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
|
|
3466
|
+
"function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
|
|
3467
|
+
"event MessageSent(bytes message)"
|
|
3468
|
+
]);
|
|
3469
|
+
var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
|
|
3470
|
+
"function deposit(uint256 amount, uint32 destinationDex) external"
|
|
3471
|
+
]);
|
|
3472
|
+
|
|
3473
|
+
// src/runtime.ts
|
|
3474
|
+
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
3475
|
+
var FUNDS_CRITICAL_THRESHOLD = 1e-3;
|
|
3476
|
+
var AgentRuntime = class {
|
|
3477
|
+
config;
|
|
3478
|
+
client;
|
|
3479
|
+
llm;
|
|
3480
|
+
strategy;
|
|
3481
|
+
executor;
|
|
3482
|
+
riskManager;
|
|
3483
|
+
marketData;
|
|
3484
|
+
vaultManager;
|
|
3485
|
+
relay = null;
|
|
3486
|
+
isRunning = false;
|
|
3487
|
+
mode = "idle";
|
|
3488
|
+
configHash;
|
|
3489
|
+
cycleCount = 0;
|
|
3490
|
+
lastCycleAt = 0;
|
|
3491
|
+
lastPortfolioValue = 0;
|
|
3492
|
+
lastEthBalance = "0";
|
|
3493
|
+
processAlive = true;
|
|
3494
|
+
riskUniverse = 0;
|
|
3495
|
+
allowedTokens = /* @__PURE__ */ new Set();
|
|
3496
|
+
// Perp trading components (null if perp not enabled)
|
|
3497
|
+
perpClient = null;
|
|
3498
|
+
perpSigner = null;
|
|
3499
|
+
perpOrders = null;
|
|
3500
|
+
perpPositions = null;
|
|
3501
|
+
perpWebSocket = null;
|
|
3502
|
+
perpRecorder = null;
|
|
3503
|
+
perpOnboarding = null;
|
|
3504
|
+
perpStrategy = null;
|
|
3505
|
+
// Two-layer perp control:
|
|
3506
|
+
// perpConnected = Hyperliquid infrastructure is initialized (WS, signer, recorder ready)
|
|
3507
|
+
// perpTradingActive = Dedicated perp trading cycle is mandated to run
|
|
3508
|
+
// When perpConnected && !perpTradingActive: agent's strategy can optionally return perp signals
|
|
3509
|
+
// When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
|
|
3510
|
+
perpConnected = false;
|
|
3511
|
+
perpTradingActive = false;
|
|
3512
|
+
constructor(config) {
|
|
3513
|
+
this.config = config;
|
|
3514
|
+
}
|
|
3515
|
+
/**
|
|
3516
|
+
* Initialize the agent runtime
|
|
3517
|
+
*/
|
|
3518
|
+
async initialize() {
|
|
3519
|
+
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
3520
|
+
this.client = new import_sdk2.ExagentClient({
|
|
3521
|
+
privateKey: this.config.privateKey,
|
|
3522
|
+
network: this.config.network
|
|
3523
|
+
});
|
|
3524
|
+
console.log(`Wallet: ${this.client.address}`);
|
|
3525
|
+
const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
|
|
3526
|
+
if (!agent) {
|
|
3527
|
+
throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
|
|
3528
|
+
}
|
|
3529
|
+
console.log(`Agent verified: ${agent.name}`);
|
|
3530
|
+
await this.ensureWalletLinked();
|
|
3531
|
+
await this.loadTradingRestrictions();
|
|
3532
|
+
console.log(`Initializing LLM: ${this.config.llm.provider}`);
|
|
3533
|
+
this.llm = await createLLMAdapter(this.config.llm);
|
|
3534
|
+
const llmMeta = this.llm.getMetadata();
|
|
3535
|
+
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
3536
|
+
await this.syncConfigHash();
|
|
3537
|
+
this.strategy = await loadStrategy();
|
|
3538
|
+
this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
|
|
3539
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
3540
|
+
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
3541
|
+
await this.initializeVaultManager();
|
|
3542
|
+
await this.initializePerp();
|
|
3543
|
+
await this.initializeRelay();
|
|
3544
|
+
console.log("Agent initialized successfully");
|
|
3545
|
+
}
|
|
3546
|
+
/**
|
|
3547
|
+
* Initialize the relay client for command center connectivity
|
|
3548
|
+
*/
|
|
3549
|
+
async initializeRelay() {
|
|
3550
|
+
const relayConfig = this.config.relay;
|
|
3551
|
+
const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
|
|
3552
|
+
if (!relayConfig?.enabled || !relayEnabled) {
|
|
3553
|
+
console.log("Relay: Disabled");
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
|
|
3557
|
+
if (!apiUrl) {
|
|
3558
|
+
console.log("Relay: No API URL configured, skipping");
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
this.relay = new RelayClient({
|
|
3562
|
+
agentId: String(this.config.agentId),
|
|
3563
|
+
privateKey: this.config.privateKey,
|
|
3564
|
+
relay: {
|
|
3565
|
+
...relayConfig,
|
|
3566
|
+
apiUrl
|
|
3567
|
+
},
|
|
3568
|
+
onCommand: (cmd) => this.handleCommand(cmd)
|
|
3569
|
+
});
|
|
3570
|
+
try {
|
|
3571
|
+
await this.relay.connect();
|
|
3572
|
+
console.log("Relay: Connected to command center");
|
|
3573
|
+
this.sendRelayStatus();
|
|
3574
|
+
} catch (error) {
|
|
3575
|
+
console.warn(
|
|
3576
|
+
"Relay: Failed to connect (agent will work locally):",
|
|
3577
|
+
error instanceof Error ? error.message : error
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Initialize the vault manager based on config
|
|
3583
|
+
*/
|
|
3584
|
+
async initializeVaultManager() {
|
|
3585
|
+
const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
|
|
3586
|
+
this.vaultManager = new VaultManager({
|
|
3587
|
+
agentId: BigInt(this.config.agentId),
|
|
3588
|
+
agentName: this.config.name,
|
|
3589
|
+
network: this.config.network,
|
|
3590
|
+
walletKey: this.config.privateKey,
|
|
3591
|
+
vaultConfig
|
|
3592
|
+
});
|
|
3593
|
+
console.log(`Vault policy: ${vaultConfig.policy}`);
|
|
3594
|
+
const status = await this.vaultManager.getVaultStatus();
|
|
3595
|
+
if (status.hasVault) {
|
|
3596
|
+
console.log(`Vault exists: ${status.vaultAddress}`);
|
|
3597
|
+
console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
|
|
3598
|
+
} else {
|
|
3599
|
+
console.log("No vault exists for this agent");
|
|
3600
|
+
if (vaultConfig.policy === "manual") {
|
|
3601
|
+
console.log("Vault creation is manual \u2014 use the command center to create one");
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Initialize Hyperliquid perp trading components.
|
|
3607
|
+
* Only initializes if perp is enabled in config AND risk universe >= 2.
|
|
3608
|
+
*/
|
|
3609
|
+
async initializePerp() {
|
|
3610
|
+
const perpConfig = this.config.perp;
|
|
3611
|
+
if (!perpConfig?.enabled) {
|
|
3612
|
+
console.log("Perp trading: Disabled");
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
if (this.riskUniverse < 2) {
|
|
3616
|
+
console.warn(`Perp trading: Blocked by risk universe ${this.riskUniverse} (need >= 2)`);
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
try {
|
|
3620
|
+
const config = {
|
|
3621
|
+
enabled: true,
|
|
3622
|
+
apiUrl: perpConfig.apiUrl || "https://api.hyperliquid.xyz",
|
|
3623
|
+
wsUrl: perpConfig.wsUrl || "wss://api.hyperliquid.xyz/ws",
|
|
3624
|
+
builderAddress: perpConfig.builderAddress,
|
|
3625
|
+
builderFeeTenthsBps: perpConfig.builderFeeTenthsBps ?? 100,
|
|
3626
|
+
maxLeverage: perpConfig.maxLeverage ?? 10,
|
|
3627
|
+
maxNotionalUSD: perpConfig.maxNotionalUSD ?? 5e4,
|
|
3628
|
+
allowedInstruments: perpConfig.allowedInstruments
|
|
3629
|
+
};
|
|
3630
|
+
this.perpClient = new HyperliquidClient(config);
|
|
3631
|
+
const perpKey = perpConfig.perpRelayerKey || this.config.privateKey;
|
|
3632
|
+
const account = (0, import_accounts5.privateKeyToAccount)(perpKey);
|
|
3633
|
+
const walletClient = (0, import_viem6.createWalletClient)({
|
|
3634
|
+
chain: { id: 42161, name: "Arbitrum", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } } },
|
|
3635
|
+
transport: (0, import_viem6.http)("https://arb1.arbitrum.io/rpc"),
|
|
3636
|
+
account
|
|
3637
|
+
});
|
|
3638
|
+
this.perpSigner = new HyperliquidSigner(walletClient);
|
|
3639
|
+
this.perpOrders = new OrderManager(this.perpClient, this.perpSigner, config);
|
|
3640
|
+
this.perpPositions = new PositionManager(this.perpClient, this.perpSigner.getAddress());
|
|
3641
|
+
this.perpOnboarding = new PerpOnboarding(this.perpClient, this.perpSigner, config);
|
|
3642
|
+
const recorderKey = perpConfig.perpRelayerKey || this.config.privateKey;
|
|
3643
|
+
this.perpRecorder = new PerpTradeRecorder({
|
|
3644
|
+
privateKey: recorderKey,
|
|
3645
|
+
rpcUrl: this.getRpcUrl(),
|
|
3646
|
+
agentId: BigInt(this.config.agentId),
|
|
3647
|
+
configHash: this.configHash
|
|
3648
|
+
});
|
|
3649
|
+
this.perpWebSocket = new HyperliquidWebSocket(config, this.perpSigner.getAddress(), this.perpClient);
|
|
3650
|
+
this.perpWebSocket.onFillReceived(async (fill) => {
|
|
3651
|
+
console.log(`Perp fill: ${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz}@${fill.px}`);
|
|
3652
|
+
const result = await this.perpRecorder.recordFill(fill);
|
|
3653
|
+
if (result.success) {
|
|
3654
|
+
this.relay?.sendMessage(
|
|
3655
|
+
"perp_fill",
|
|
3656
|
+
"success",
|
|
3657
|
+
"Perp Fill",
|
|
3658
|
+
`${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz} @ $${fill.px}`,
|
|
3659
|
+
{ instrument: fill.coin, side: fill.side, size: fill.sz, price: fill.px, txHash: result.txHash }
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
});
|
|
3663
|
+
this.perpWebSocket.onLiquidationDetected((instrument, size) => {
|
|
3664
|
+
console.error(`LIQUIDATION: ${instrument} position (${size}) was liquidated`);
|
|
3665
|
+
this.relay?.sendMessage(
|
|
3666
|
+
"perp_liquidation_warning",
|
|
3667
|
+
"error",
|
|
3668
|
+
"Position Liquidated",
|
|
3669
|
+
`${instrument} position of ${Math.abs(size)} was liquidated.`,
|
|
3670
|
+
{ instrument, size }
|
|
3671
|
+
);
|
|
3672
|
+
});
|
|
3673
|
+
this.perpWebSocket.onFundingReceived((funding) => {
|
|
3674
|
+
const amount = parseFloat(funding.usdc);
|
|
3675
|
+
if (Math.abs(amount) > 0.01) {
|
|
3676
|
+
this.relay?.sendMessage(
|
|
3677
|
+
"perp_funding",
|
|
3678
|
+
"info",
|
|
3679
|
+
"Funding Payment",
|
|
3680
|
+
`${funding.coin}: ${amount > 0 ? "+" : ""}$${amount.toFixed(4)}`,
|
|
3681
|
+
{ instrument: funding.coin, amount: funding.usdc, rate: funding.fundingRate }
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
});
|
|
3685
|
+
const onboardingStatus = await this.perpOnboarding.onboard(this.riskUniverse);
|
|
3686
|
+
if (!onboardingStatus.ready) {
|
|
3687
|
+
console.warn(`Perp onboarding incomplete \u2014 trading will be limited`);
|
|
3688
|
+
if (!onboardingStatus.hasBalance) {
|
|
3689
|
+
console.warn(" No USDC balance on Hyperliquid \u2014 deposit required");
|
|
3690
|
+
}
|
|
3691
|
+
if (!onboardingStatus.builderFeeApproved) {
|
|
3692
|
+
console.warn(" Builder fee not approved \u2014 orders may fail");
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
try {
|
|
3696
|
+
await this.perpWebSocket.connect();
|
|
3697
|
+
console.log("Perp WebSocket: Connected");
|
|
3698
|
+
} catch (error) {
|
|
3699
|
+
console.warn("Perp WebSocket: Failed to connect (will retry):", error instanceof Error ? error.message : error);
|
|
3700
|
+
}
|
|
3701
|
+
this.perpConnected = true;
|
|
3702
|
+
console.log(`Hyperliquid: Connected (${config.allowedInstruments?.join(", ") || "all instruments"})`);
|
|
3703
|
+
console.log(` Builder: ${config.builderAddress} (${config.builderFeeTenthsBps / 10} bps)`);
|
|
3704
|
+
console.log(` Max leverage: ${config.maxLeverage}x, Max notional: $${config.maxNotionalUSD.toLocaleString()}`);
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3707
|
+
console.error(`Perp initialization failed: ${message}`);
|
|
3708
|
+
console.warn("Perp trading will be disabled for this session");
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Ensure the current wallet is linked to the agent.
|
|
3713
|
+
* If the trading wallet differs from the owner, enters a recovery loop
|
|
3714
|
+
* that waits for the owner to link it from the website.
|
|
3715
|
+
*/
|
|
3716
|
+
async ensureWalletLinked() {
|
|
3717
|
+
const agentId = BigInt(this.config.agentId);
|
|
3718
|
+
const address = this.client.address;
|
|
3719
|
+
const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
|
|
2232
3720
|
if (!isLinked) {
|
|
2233
3721
|
console.log("Wallet not linked, linking now...");
|
|
2234
3722
|
const agent = await this.client.registry.getAgent(agentId);
|
|
@@ -2339,10 +3827,10 @@ var AgentRuntime = class {
|
|
|
2339
3827
|
const message = error instanceof Error ? error.message : String(error);
|
|
2340
3828
|
if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
2341
3829
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
2342
|
-
const chain =
|
|
2343
|
-
const publicClientInstance = (0,
|
|
3830
|
+
const chain = import_chains4.base;
|
|
3831
|
+
const publicClientInstance = (0, import_viem6.createPublicClient)({
|
|
2344
3832
|
chain,
|
|
2345
|
-
transport: (0,
|
|
3833
|
+
transport: (0, import_viem6.http)(this.getRpcUrl())
|
|
2346
3834
|
});
|
|
2347
3835
|
console.log("");
|
|
2348
3836
|
console.log("=== ETH NEEDED FOR GAS ===");
|
|
@@ -2553,6 +4041,131 @@ var AgentRuntime = class {
|
|
|
2553
4041
|
}
|
|
2554
4042
|
break;
|
|
2555
4043
|
}
|
|
4044
|
+
case "enable_hyperliquid":
|
|
4045
|
+
if (this.perpConnected) {
|
|
4046
|
+
this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already connected");
|
|
4047
|
+
break;
|
|
4048
|
+
}
|
|
4049
|
+
if (!this.config.perp?.enabled) {
|
|
4050
|
+
this.relay?.sendCommandResult(cmd.id, false, "Perp trading not configured in agent config");
|
|
4051
|
+
break;
|
|
4052
|
+
}
|
|
4053
|
+
if (this.riskUniverse < 2) {
|
|
4054
|
+
this.relay?.sendCommandResult(cmd.id, false, `Risk universe ${this.riskUniverse} too low (need >= 2)`);
|
|
4055
|
+
break;
|
|
4056
|
+
}
|
|
4057
|
+
try {
|
|
4058
|
+
await this.initializePerp();
|
|
4059
|
+
if (this.perpConnected) {
|
|
4060
|
+
this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid connected");
|
|
4061
|
+
this.relay?.sendMessage(
|
|
4062
|
+
"system",
|
|
4063
|
+
"success",
|
|
4064
|
+
"Hyperliquid Enabled",
|
|
4065
|
+
'Hyperliquid infrastructure connected. Agent can now include perp signals in its strategy. Use "Start Perp Trading" to mandate a dedicated perp cycle.'
|
|
4066
|
+
);
|
|
4067
|
+
} else {
|
|
4068
|
+
this.relay?.sendCommandResult(cmd.id, false, "Failed to connect to Hyperliquid");
|
|
4069
|
+
}
|
|
4070
|
+
} catch (error) {
|
|
4071
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
4072
|
+
this.relay?.sendCommandResult(cmd.id, false, `Hyperliquid init failed: ${msg}`);
|
|
4073
|
+
}
|
|
4074
|
+
this.sendRelayStatus();
|
|
4075
|
+
break;
|
|
4076
|
+
case "disable_hyperliquid":
|
|
4077
|
+
if (!this.perpConnected) {
|
|
4078
|
+
this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already disconnected");
|
|
4079
|
+
break;
|
|
4080
|
+
}
|
|
4081
|
+
this.perpTradingActive = false;
|
|
4082
|
+
if (this.perpWebSocket) {
|
|
4083
|
+
this.perpWebSocket.disconnect();
|
|
4084
|
+
}
|
|
4085
|
+
if (this.perpRecorder) {
|
|
4086
|
+
this.perpRecorder.stop();
|
|
4087
|
+
}
|
|
4088
|
+
this.perpConnected = false;
|
|
4089
|
+
console.log("Hyperliquid disabled via command center");
|
|
4090
|
+
this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid disconnected");
|
|
4091
|
+
this.relay?.sendMessage(
|
|
4092
|
+
"system",
|
|
4093
|
+
"info",
|
|
4094
|
+
"Hyperliquid Disabled",
|
|
4095
|
+
"Hyperliquid infrastructure disconnected. Agent will trade spot only."
|
|
4096
|
+
);
|
|
4097
|
+
this.sendRelayStatus();
|
|
4098
|
+
break;
|
|
4099
|
+
case "start_perp_trading":
|
|
4100
|
+
if (!this.perpConnected) {
|
|
4101
|
+
this.relay?.sendCommandResult(cmd.id, false, "Hyperliquid not connected. Enable Hyperliquid first.");
|
|
4102
|
+
break;
|
|
4103
|
+
}
|
|
4104
|
+
if (this.perpTradingActive) {
|
|
4105
|
+
this.relay?.sendCommandResult(cmd.id, true, "Perp trading already active");
|
|
4106
|
+
break;
|
|
4107
|
+
}
|
|
4108
|
+
this.perpTradingActive = true;
|
|
4109
|
+
if (this.mode !== "trading") {
|
|
4110
|
+
this.mode = "trading";
|
|
4111
|
+
this.isRunning = true;
|
|
4112
|
+
}
|
|
4113
|
+
console.log("Perp trading mandated via command center");
|
|
4114
|
+
this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle active");
|
|
4115
|
+
this.relay?.sendMessage(
|
|
4116
|
+
"system",
|
|
4117
|
+
"success",
|
|
4118
|
+
"Perp Trading Active",
|
|
4119
|
+
"Dedicated perp trading cycle is now running every interval."
|
|
4120
|
+
);
|
|
4121
|
+
this.sendRelayStatus();
|
|
4122
|
+
break;
|
|
4123
|
+
case "stop_perp_trading":
|
|
4124
|
+
if (!this.perpTradingActive) {
|
|
4125
|
+
this.relay?.sendCommandResult(cmd.id, true, "Perp trading already stopped");
|
|
4126
|
+
break;
|
|
4127
|
+
}
|
|
4128
|
+
this.perpTradingActive = false;
|
|
4129
|
+
console.log("Perp trading cycle stopped via command center");
|
|
4130
|
+
this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle stopped");
|
|
4131
|
+
this.relay?.sendMessage(
|
|
4132
|
+
"system",
|
|
4133
|
+
"info",
|
|
4134
|
+
"Perp Trading Stopped",
|
|
4135
|
+
"Dedicated perp cycle stopped. Hyperliquid remains connected \u2014 strategy can still include perp signals."
|
|
4136
|
+
);
|
|
4137
|
+
this.sendRelayStatus();
|
|
4138
|
+
break;
|
|
4139
|
+
case "update_perp_params": {
|
|
4140
|
+
const perpParams = cmd.params || {};
|
|
4141
|
+
let perpUpdated = false;
|
|
4142
|
+
if (this.config.perp && perpParams.maxLeverage !== void 0) {
|
|
4143
|
+
const val = Number(perpParams.maxLeverage);
|
|
4144
|
+
if (val >= 1 && val <= 50) {
|
|
4145
|
+
this.config.perp.maxLeverage = val;
|
|
4146
|
+
perpUpdated = true;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
if (this.config.perp && perpParams.maxNotionalUSD !== void 0) {
|
|
4150
|
+
const val = Number(perpParams.maxNotionalUSD);
|
|
4151
|
+
if (val >= 100) {
|
|
4152
|
+
this.config.perp.maxNotionalUSD = val;
|
|
4153
|
+
perpUpdated = true;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
if (perpUpdated) {
|
|
4157
|
+
this.relay?.sendCommandResult(cmd.id, true, "Perp parameters updated");
|
|
4158
|
+
this.relay?.sendMessage(
|
|
4159
|
+
"config_updated",
|
|
4160
|
+
"info",
|
|
4161
|
+
"Perp Params Updated",
|
|
4162
|
+
`Max leverage: ${this.config.perp?.maxLeverage}x, Max notional: $${this.config.perp?.maxNotionalUSD?.toLocaleString()}`
|
|
4163
|
+
);
|
|
4164
|
+
} else {
|
|
4165
|
+
this.relay?.sendCommandResult(cmd.id, false, "No valid perp parameters provided");
|
|
4166
|
+
}
|
|
4167
|
+
break;
|
|
4168
|
+
}
|
|
2556
4169
|
case "refresh_status":
|
|
2557
4170
|
this.sendRelayStatus();
|
|
2558
4171
|
this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
|
|
@@ -2607,8 +4220,33 @@ var AgentRuntime = class {
|
|
|
2607
4220
|
policy: vaultConfig.policy,
|
|
2608
4221
|
hasVault: false,
|
|
2609
4222
|
vaultAddress: null
|
|
2610
|
-
}
|
|
4223
|
+
},
|
|
4224
|
+
perp: this.perpConnected ? {
|
|
4225
|
+
enabled: true,
|
|
4226
|
+
trading: this.perpTradingActive,
|
|
4227
|
+
equity: 0,
|
|
4228
|
+
unrealizedPnl: 0,
|
|
4229
|
+
marginUsed: 0,
|
|
4230
|
+
openPositions: 0,
|
|
4231
|
+
effectiveLeverage: 0,
|
|
4232
|
+
pendingRecords: this.perpRecorder?.pendingRetries ?? 0
|
|
4233
|
+
} : void 0
|
|
2611
4234
|
};
|
|
4235
|
+
if (this.perpConnected && this.perpPositions && status.perp) {
|
|
4236
|
+
this.perpPositions.getAccountSummary().then((account) => {
|
|
4237
|
+
if (status.perp) {
|
|
4238
|
+
status.perp.equity = account.totalEquity;
|
|
4239
|
+
status.perp.unrealizedPnl = account.totalUnrealizedPnl;
|
|
4240
|
+
status.perp.marginUsed = account.totalMarginUsed;
|
|
4241
|
+
status.perp.effectiveLeverage = account.effectiveLeverage;
|
|
4242
|
+
}
|
|
4243
|
+
}).catch(() => {
|
|
4244
|
+
});
|
|
4245
|
+
this.perpPositions.getPositionCount().then((count) => {
|
|
4246
|
+
if (status.perp) status.perp.openPositions = count;
|
|
4247
|
+
}).catch(() => {
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
2612
4250
|
this.relay.sendHeartbeat(status);
|
|
2613
4251
|
}
|
|
2614
4252
|
/**
|
|
@@ -2707,8 +4345,75 @@ var AgentRuntime = class {
|
|
|
2707
4345
|
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2708
4346
|
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
2709
4347
|
}
|
|
4348
|
+
if (this.perpConnected && this.perpTradingActive) {
|
|
4349
|
+
try {
|
|
4350
|
+
await this.runPerpCycle();
|
|
4351
|
+
} catch (error) {
|
|
4352
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4353
|
+
console.error("Error in perp cycle:", message);
|
|
4354
|
+
this.relay?.sendMessage("system", "error", "Perp Cycle Error", message);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
2710
4357
|
this.sendRelayStatus();
|
|
2711
4358
|
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Run a single perp trading cycle.
|
|
4361
|
+
* Fetches market data, positions, calls perp strategy, applies risk filters, executes.
|
|
4362
|
+
* Fills arrive async via WebSocket and are auto-recorded on Base.
|
|
4363
|
+
*/
|
|
4364
|
+
async runPerpCycle() {
|
|
4365
|
+
if (!this.perpClient || !this.perpPositions || !this.perpOrders || !this.perpConnected) return;
|
|
4366
|
+
const perpConfig = this.config.perp;
|
|
4367
|
+
if (!perpConfig?.enabled) return;
|
|
4368
|
+
console.log(" [PERP] Running perp cycle...");
|
|
4369
|
+
const [positions, account] = await Promise.all([
|
|
4370
|
+
this.perpPositions.getPositions(true),
|
|
4371
|
+
this.perpPositions.getAccountSummary(true)
|
|
4372
|
+
]);
|
|
4373
|
+
console.log(` [PERP] Equity: $${account.totalEquity.toFixed(2)}, Positions: ${positions.length}, Leverage: ${account.effectiveLeverage.toFixed(1)}x`);
|
|
4374
|
+
const dangerousPositions = await this.perpPositions.getDangerousPositions(0.7);
|
|
4375
|
+
for (const pos of dangerousPositions) {
|
|
4376
|
+
console.warn(` [PERP] WARNING: ${pos.instrument} near liquidation`);
|
|
4377
|
+
this.relay?.sendMessage(
|
|
4378
|
+
"perp_liquidation_warning",
|
|
4379
|
+
"warning",
|
|
4380
|
+
"Near Liquidation",
|
|
4381
|
+
`${pos.instrument} ${pos.size > 0 ? "LONG" : "SHORT"} \u2014 close to liquidation price $${pos.liquidationPrice.toFixed(2)}`,
|
|
4382
|
+
{ instrument: pos.instrument, liquidationPrice: pos.liquidationPrice, markPrice: pos.markPrice }
|
|
4383
|
+
);
|
|
4384
|
+
}
|
|
4385
|
+
const instruments = perpConfig.allowedInstruments || ["ETH", "BTC", "SOL"];
|
|
4386
|
+
const marketData = await this.perpClient.getMarketData(instruments);
|
|
4387
|
+
if (!this.perpStrategy) {
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
let signals;
|
|
4391
|
+
try {
|
|
4392
|
+
signals = await this.perpStrategy(marketData, positions, account, this.llm, this.config);
|
|
4393
|
+
} catch (error) {
|
|
4394
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4395
|
+
console.error(" [PERP] Strategy error:", message);
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
console.log(` [PERP] Strategy generated ${signals.length} signals`);
|
|
4399
|
+
const maxLeverage = perpConfig.maxLeverage ?? 10;
|
|
4400
|
+
const maxNotionalUSD = perpConfig.maxNotionalUSD ?? 5e4;
|
|
4401
|
+
const filteredSignals = this.riskManager.filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD);
|
|
4402
|
+
console.log(` [PERP] ${filteredSignals.length} signals passed risk checks`);
|
|
4403
|
+
for (const signal of filteredSignals) {
|
|
4404
|
+
if (signal.action === "hold") continue;
|
|
4405
|
+
if (signal.price === 0) {
|
|
4406
|
+
const md = marketData.find((m) => m.instrument === signal.instrument);
|
|
4407
|
+
if (md) signal.price = md.midPrice;
|
|
4408
|
+
}
|
|
4409
|
+
const result = await this.perpOrders.placeOrder(signal);
|
|
4410
|
+
if (result.success) {
|
|
4411
|
+
console.log(` [PERP] Order placed: ${signal.instrument} ${signal.action} \u2014 ${result.status}`);
|
|
4412
|
+
} else {
|
|
4413
|
+
console.warn(` [PERP] Order failed: ${signal.instrument} ${signal.action} \u2014 ${result.error}`);
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
2712
4417
|
/**
|
|
2713
4418
|
* Check if ETH balance is below threshold and notify.
|
|
2714
4419
|
* Returns true if trading should continue, false if ETH is critically low.
|
|
@@ -2754,6 +4459,12 @@ var AgentRuntime = class {
|
|
|
2754
4459
|
this.isRunning = false;
|
|
2755
4460
|
this.processAlive = false;
|
|
2756
4461
|
this.mode = "idle";
|
|
4462
|
+
if (this.perpWebSocket) {
|
|
4463
|
+
this.perpWebSocket.disconnect();
|
|
4464
|
+
}
|
|
4465
|
+
if (this.perpRecorder) {
|
|
4466
|
+
this.perpRecorder.stop();
|
|
4467
|
+
}
|
|
2757
4468
|
if (this.relay) {
|
|
2758
4469
|
this.relay.disconnect();
|
|
2759
4470
|
}
|
|
@@ -3094,12 +4805,21 @@ function loadSecureEnv(basePath, passphrase) {
|
|
|
3094
4805
|
DeepSeekAdapter,
|
|
3095
4806
|
GoogleAdapter,
|
|
3096
4807
|
GroqAdapter,
|
|
4808
|
+
HYPERLIQUID_DOMAIN,
|
|
4809
|
+
HyperliquidClient,
|
|
4810
|
+
HyperliquidSigner,
|
|
4811
|
+
HyperliquidWebSocket,
|
|
3097
4812
|
LLMConfigSchema,
|
|
3098
4813
|
LLMProviderSchema,
|
|
3099
4814
|
MarketDataService,
|
|
3100
4815
|
MistralAdapter,
|
|
3101
4816
|
OllamaAdapter,
|
|
3102
4817
|
OpenAIAdapter,
|
|
4818
|
+
OrderManager,
|
|
4819
|
+
PerpConfigSchema,
|
|
4820
|
+
PerpOnboarding,
|
|
4821
|
+
PerpTradeRecorder,
|
|
4822
|
+
PositionManager,
|
|
3103
4823
|
RelayClient,
|
|
3104
4824
|
RelayConfigSchema,
|
|
3105
4825
|
RiskManager,
|
|
@@ -3115,7 +4835,10 @@ function loadSecureEnv(basePath, passphrase) {
|
|
|
3115
4835
|
createSampleConfig,
|
|
3116
4836
|
decryptEnvFile,
|
|
3117
4837
|
encryptEnvFile,
|
|
4838
|
+
fillHashToBytes32,
|
|
4839
|
+
fillOidToBytes32,
|
|
3118
4840
|
getAllStrategyTemplates,
|
|
4841
|
+
getNextNonce,
|
|
3119
4842
|
getStrategyTemplate,
|
|
3120
4843
|
loadConfig,
|
|
3121
4844
|
loadSecureEnv,
|