@exagent/agent 0.1.20 → 0.1.22
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-EJHDRG5Y.mjs +5305 -0
- package/dist/chunk-N6RIRCGH.mjs +5221 -0
- package/dist/chunk-NFE6HTL3.mjs +5218 -0
- package/dist/chunk-NIZP5EVK.mjs +5226 -0
- package/dist/chunk-U6YJHCO3.mjs +5226 -0
- package/dist/cli.js +1522 -1120
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +204 -6
- package/dist/index.d.ts +204 -6
- package/dist/index.js +976 -570
- package/dist/index.mjs +5 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -31,105 +31,641 @@ var fs2 = __toESM(require("fs"));
|
|
|
31
31
|
var path2 = __toESM(require("path"));
|
|
32
32
|
|
|
33
33
|
// src/runtime.ts
|
|
34
|
-
var
|
|
34
|
+
var import_sdk = require("@exagent/sdk");
|
|
35
35
|
var import_viem6 = require("viem");
|
|
36
36
|
var import_chains4 = require("viem/chains");
|
|
37
37
|
var import_accounts5 = require("viem/accounts");
|
|
38
38
|
|
|
39
|
-
// src/
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
// src/trading/market.ts
|
|
40
|
+
var import_viem = require("viem");
|
|
41
|
+
var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
42
|
+
var TOKEN_DECIMALS = {
|
|
43
|
+
// Base Mainnet — Core tokens
|
|
44
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
45
|
+
// USDC
|
|
46
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
47
|
+
// USDbC
|
|
48
|
+
"0x4200000000000000000000000000000000000006": 18,
|
|
49
|
+
// WETH
|
|
50
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
51
|
+
// DAI
|
|
52
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
53
|
+
// cbETH
|
|
54
|
+
[NATIVE_ETH.toLowerCase()]: 18,
|
|
55
|
+
// Native ETH
|
|
56
|
+
// Base Mainnet — Established tokens
|
|
57
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
58
|
+
// AERO (Aerodrome)
|
|
59
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
60
|
+
// BRETT
|
|
61
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
62
|
+
// DEGEN
|
|
63
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
64
|
+
// VIRTUAL
|
|
65
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
66
|
+
// TOSHI
|
|
67
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
68
|
+
// cbBTC
|
|
69
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
70
|
+
// ezETH (Renzo)
|
|
71
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
72
|
+
// wstETH (Lido)
|
|
73
|
+
};
|
|
74
|
+
function getTokenDecimals(address) {
|
|
75
|
+
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
76
|
+
if (decimals === void 0) {
|
|
77
|
+
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
78
|
+
return 18;
|
|
47
79
|
}
|
|
48
|
-
|
|
80
|
+
return decimals;
|
|
81
|
+
}
|
|
82
|
+
var TOKEN_TO_COINGECKO = {
|
|
83
|
+
// Core
|
|
84
|
+
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
85
|
+
// WETH
|
|
86
|
+
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
87
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
88
|
+
// USDC
|
|
89
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
90
|
+
// USDbC
|
|
91
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
92
|
+
// cbETH
|
|
93
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
94
|
+
// DAI
|
|
95
|
+
// Established
|
|
96
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
97
|
+
// AERO
|
|
98
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
99
|
+
// BRETT
|
|
100
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
101
|
+
// DEGEN
|
|
102
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
103
|
+
// VIRTUAL
|
|
104
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
105
|
+
// TOSHI
|
|
106
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
107
|
+
// cbBTC
|
|
108
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
109
|
+
// ezETH
|
|
110
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
111
|
+
// wstETH
|
|
112
|
+
};
|
|
113
|
+
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
114
|
+
var PRICE_STALENESS_MS = 6e4;
|
|
115
|
+
var MarketDataService = class {
|
|
116
|
+
rpcUrl;
|
|
117
|
+
client;
|
|
118
|
+
/** Cached prices from last fetch */
|
|
119
|
+
cachedPrices = {};
|
|
120
|
+
/** Timestamp of last successful price fetch */
|
|
121
|
+
lastPriceFetchAt = 0;
|
|
122
|
+
constructor(rpcUrl) {
|
|
123
|
+
this.rpcUrl = rpcUrl;
|
|
124
|
+
this.client = (0, import_viem.createPublicClient)({
|
|
125
|
+
transport: (0, import_viem.http)(rpcUrl)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Cached volume data */
|
|
129
|
+
cachedVolume24h = {};
|
|
130
|
+
/** Cached price change data */
|
|
131
|
+
cachedPriceChange24h = {};
|
|
132
|
+
/**
|
|
133
|
+
* Fetch current market data for the agent
|
|
134
|
+
*/
|
|
135
|
+
async fetchMarketData(walletAddress, tokenAddresses) {
|
|
136
|
+
const prices = await this.fetchPrices(tokenAddresses);
|
|
137
|
+
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
138
|
+
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
139
|
+
let gasPrice;
|
|
140
|
+
try {
|
|
141
|
+
gasPrice = await this.client.getGasPrice();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
49
144
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
prices,
|
|
147
|
+
balances,
|
|
148
|
+
portfolioValue,
|
|
149
|
+
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
150
|
+
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
151
|
+
gasPrice,
|
|
152
|
+
network: {
|
|
153
|
+
chainId: this.client.chain?.id ?? 8453
|
|
154
|
+
}
|
|
53
155
|
};
|
|
54
156
|
}
|
|
55
157
|
/**
|
|
56
|
-
*
|
|
158
|
+
* Check if cached prices are still fresh
|
|
57
159
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return `Local (${this.config.model || "ollama"})`;
|
|
61
|
-
}
|
|
62
|
-
return this.config.model || this.config.provider;
|
|
160
|
+
get pricesAreFresh() {
|
|
161
|
+
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
63
162
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Fetch token prices from CoinGecko free API
|
|
165
|
+
* Returns cached prices if still fresh (<60s old)
|
|
166
|
+
*/
|
|
167
|
+
async fetchPrices(tokenAddresses) {
|
|
168
|
+
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
169
|
+
const prices2 = { ...this.cachedPrices };
|
|
170
|
+
for (const addr of tokenAddresses) {
|
|
171
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
172
|
+
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
173
|
+
prices2[addr.toLowerCase()] = 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return prices2;
|
|
73
177
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const response = await this.client.chat.completions.create({
|
|
82
|
-
model: this.config.model || "gpt-4.1",
|
|
83
|
-
messages: messages.map((m) => ({
|
|
84
|
-
role: m.role,
|
|
85
|
-
content: m.content
|
|
86
|
-
})),
|
|
87
|
-
temperature: this.config.temperature,
|
|
88
|
-
max_tokens: this.config.maxTokens
|
|
89
|
-
});
|
|
90
|
-
const choice = response.choices[0];
|
|
91
|
-
if (!choice || !choice.message) {
|
|
92
|
-
throw new Error("No response from OpenAI");
|
|
178
|
+
const prices = {};
|
|
179
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
180
|
+
for (const addr of tokenAddresses) {
|
|
181
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
182
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
183
|
+
idsToFetch.add(cgId);
|
|
93
184
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
185
|
+
}
|
|
186
|
+
idsToFetch.add("ethereum");
|
|
187
|
+
if (idsToFetch.size > 0) {
|
|
188
|
+
try {
|
|
189
|
+
const ids = Array.from(idsToFetch).join(",");
|
|
190
|
+
const response = await fetch(
|
|
191
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
192
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
193
|
+
);
|
|
194
|
+
if (response.ok) {
|
|
195
|
+
const data = await response.json();
|
|
196
|
+
for (const [cgId, priceData] of Object.entries(data)) {
|
|
197
|
+
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
198
|
+
if (id === cgId) {
|
|
199
|
+
const key = addr.toLowerCase();
|
|
200
|
+
prices[key] = priceData.usd;
|
|
201
|
+
if (priceData.usd_24h_vol !== void 0) {
|
|
202
|
+
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
203
|
+
}
|
|
204
|
+
if (priceData.usd_24h_change !== void 0) {
|
|
205
|
+
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.lastPriceFetchAt = Date.now();
|
|
211
|
+
} else {
|
|
212
|
+
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
105
216
|
}
|
|
106
|
-
throw error;
|
|
107
217
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
apiKey;
|
|
114
|
-
baseUrl;
|
|
115
|
-
constructor(config) {
|
|
116
|
-
super(config);
|
|
117
|
-
if (!config.apiKey) {
|
|
118
|
-
throw new Error("Anthropic API key required");
|
|
218
|
+
for (const addr of tokenAddresses) {
|
|
219
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
220
|
+
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
221
|
+
prices[addr.toLowerCase()] = 1;
|
|
222
|
+
}
|
|
119
223
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
const missingAddrs = tokenAddresses.filter(
|
|
225
|
+
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
226
|
+
);
|
|
227
|
+
if (missingAddrs.length > 0) {
|
|
228
|
+
try {
|
|
229
|
+
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
230
|
+
const llamaResponse = await fetch(
|
|
231
|
+
`https://coins.llama.fi/prices/current/${coins}`,
|
|
232
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
233
|
+
);
|
|
234
|
+
if (llamaResponse.ok) {
|
|
235
|
+
const llamaData = await llamaResponse.json();
|
|
236
|
+
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
237
|
+
const addr = key.replace("base:", "").toLowerCase();
|
|
238
|
+
if (data.price && data.confidence > 0.5) {
|
|
239
|
+
prices[addr] = data.price;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (Object.keys(prices).length > 0) {
|
|
248
|
+
this.cachedPrices = prices;
|
|
249
|
+
}
|
|
250
|
+
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
251
|
+
console.warn("Using cached prices (last successful fetch was stale)");
|
|
252
|
+
return { ...this.cachedPrices };
|
|
253
|
+
}
|
|
254
|
+
for (const addr of tokenAddresses) {
|
|
255
|
+
if (!prices[addr.toLowerCase()]) {
|
|
256
|
+
console.warn(`No price available for ${addr}, using 0`);
|
|
257
|
+
prices[addr.toLowerCase()] = 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return prices;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Fetch real on-chain balances: native ETH + ERC-20 tokens
|
|
264
|
+
*/
|
|
265
|
+
async fetchBalances(walletAddress, tokenAddresses) {
|
|
266
|
+
const balances = {};
|
|
267
|
+
const wallet = walletAddress;
|
|
268
|
+
try {
|
|
269
|
+
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
270
|
+
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
271
|
+
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
272
|
+
try {
|
|
273
|
+
const balance = await this.client.readContract({
|
|
274
|
+
address: tokenAddress,
|
|
275
|
+
abi: import_viem.erc20Abi,
|
|
276
|
+
functionName: "balanceOf",
|
|
277
|
+
args: [wallet]
|
|
278
|
+
});
|
|
279
|
+
return { address: tokenAddress.toLowerCase(), balance };
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const results = await Promise.all(erc20Promises);
|
|
285
|
+
for (const { address, balance } of results) {
|
|
286
|
+
balances[address] = balance;
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
290
|
+
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
291
|
+
for (const address of tokenAddresses) {
|
|
292
|
+
balances[address.toLowerCase()] = 0n;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return balances;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Calculate total portfolio value in USD
|
|
299
|
+
*/
|
|
300
|
+
calculatePortfolioValue(balances, prices) {
|
|
301
|
+
let total = 0;
|
|
302
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
303
|
+
const price = prices[address.toLowerCase()] || 0;
|
|
304
|
+
const decimals = getTokenDecimals(address);
|
|
305
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
306
|
+
total += amount * price;
|
|
307
|
+
}
|
|
308
|
+
return total;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/position-tracker.ts
|
|
313
|
+
var BASE_ASSETS = /* @__PURE__ */ new Set([
|
|
314
|
+
NATIVE_ETH.toLowerCase(),
|
|
315
|
+
// Native ETH
|
|
316
|
+
"0x4200000000000000000000000000000000000006",
|
|
317
|
+
// WETH
|
|
318
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
319
|
+
// USDC
|
|
320
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
|
|
321
|
+
// USDbC
|
|
322
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
|
|
323
|
+
// DAI
|
|
324
|
+
"0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
|
|
325
|
+
// USDT
|
|
326
|
+
"0x60a3e35cc302bfa44cb36dc100b2587cd09b9c83"
|
|
327
|
+
// EURC
|
|
328
|
+
]);
|
|
329
|
+
var TOKEN_SYMBOLS = {
|
|
330
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "AERO",
|
|
331
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "BRETT",
|
|
332
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "DEGEN",
|
|
333
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "VIRTUAL",
|
|
334
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "TOSHI",
|
|
335
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "cbBTC",
|
|
336
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "ezETH",
|
|
337
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wstETH",
|
|
338
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "cbETH",
|
|
339
|
+
"0x13403fb738c97cf7564f279288468c140aaed05c": "EXA"
|
|
340
|
+
};
|
|
341
|
+
var KEY_POSITIONS = "__positions";
|
|
342
|
+
var KEY_TRADE_HISTORY = "__trade_history";
|
|
343
|
+
var KEY_RISK_STATE = "__risk_state";
|
|
344
|
+
var PositionTracker = class {
|
|
345
|
+
store;
|
|
346
|
+
positions;
|
|
347
|
+
tradeHistory;
|
|
348
|
+
maxTradeHistory;
|
|
349
|
+
constructor(store, options) {
|
|
350
|
+
this.store = store;
|
|
351
|
+
this.maxTradeHistory = options?.maxTradeHistory ?? 50;
|
|
352
|
+
this.positions = store.get(KEY_POSITIONS) || {};
|
|
353
|
+
this.tradeHistory = store.get(KEY_TRADE_HISTORY) || [];
|
|
354
|
+
const posCount = Object.keys(this.positions).length;
|
|
355
|
+
if (posCount > 0 || this.tradeHistory.length > 0) {
|
|
356
|
+
console.log(`Position tracker loaded: ${posCount} positions, ${this.tradeHistory.length} trade records`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// ============================================================
|
|
360
|
+
// TRADE RECORDING (called by runtime after execution)
|
|
361
|
+
// ============================================================
|
|
362
|
+
/**
|
|
363
|
+
* Record a trade result. On buy: creates/updates position with cost-basis
|
|
364
|
+
* weighted average. On sell: calculates realized PnL and removes if fully sold.
|
|
365
|
+
*/
|
|
366
|
+
recordTrade(params) {
|
|
367
|
+
const { action, tokenIn, tokenOut, amountIn, priceIn, priceOut, txHash, reasoning, success } = params;
|
|
368
|
+
const decimalsIn = getTokenDecimals(tokenIn);
|
|
369
|
+
const amountInUnits = Number(amountIn) / Math.pow(10, decimalsIn);
|
|
370
|
+
const tradeValueUSD = amountInUnits * priceIn;
|
|
371
|
+
const record = {
|
|
372
|
+
timestamp: Date.now(),
|
|
373
|
+
action,
|
|
374
|
+
tokenIn: tokenIn.toLowerCase(),
|
|
375
|
+
tokenOut: tokenOut.toLowerCase(),
|
|
376
|
+
amountIn: amountIn.toString(),
|
|
377
|
+
priceUSD: tradeValueUSD,
|
|
378
|
+
txHash,
|
|
379
|
+
reasoning,
|
|
380
|
+
success
|
|
381
|
+
};
|
|
382
|
+
if (success) {
|
|
383
|
+
if (action === "buy") {
|
|
384
|
+
this.handleBuy(tokenOut.toLowerCase(), tradeValueUSD, priceOut, txHash);
|
|
385
|
+
} else if (action === "sell") {
|
|
386
|
+
const realizedPnL = this.handleSell(tokenIn.toLowerCase(), tradeValueUSD);
|
|
387
|
+
record.realizedPnL = realizedPnL;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.tradeHistory.unshift(record);
|
|
391
|
+
if (this.tradeHistory.length > this.maxTradeHistory) {
|
|
392
|
+
this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
|
|
393
|
+
}
|
|
394
|
+
this.persist();
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Handle a buy: create or update position with cost-basis weighted average.
|
|
398
|
+
*/
|
|
399
|
+
handleBuy(token, costUSD, priceUSD, txHash) {
|
|
400
|
+
if (BASE_ASSETS.has(token) || priceUSD <= 0 || costUSD <= 0) return;
|
|
401
|
+
const existing = this.positions[token];
|
|
402
|
+
const acquiredAmount = costUSD / priceUSD;
|
|
403
|
+
if (existing) {
|
|
404
|
+
const newTotalCost = existing.totalCostBasis + costUSD;
|
|
405
|
+
const newTotalAmount = existing.totalAmountAcquired + acquiredAmount;
|
|
406
|
+
existing.averageEntryPrice = newTotalAmount > 0 ? newTotalCost / newTotalAmount : priceUSD;
|
|
407
|
+
existing.totalCostBasis = newTotalCost;
|
|
408
|
+
existing.totalAmountAcquired = newTotalAmount;
|
|
409
|
+
existing.lastUpdateTimestamp = Date.now();
|
|
410
|
+
if (txHash) {
|
|
411
|
+
existing.txHashes.push(txHash);
|
|
412
|
+
if (existing.txHashes.length > 10) existing.txHashes.shift();
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
this.positions[token] = {
|
|
416
|
+
token,
|
|
417
|
+
symbol: TOKEN_SYMBOLS[token],
|
|
418
|
+
entryPrice: priceUSD,
|
|
419
|
+
averageEntryPrice: priceUSD,
|
|
420
|
+
totalCostBasis: costUSD,
|
|
421
|
+
totalAmountAcquired: acquiredAmount,
|
|
422
|
+
currentAmount: acquiredAmount,
|
|
423
|
+
entryTimestamp: Date.now(),
|
|
424
|
+
lastUpdateTimestamp: Date.now(),
|
|
425
|
+
txHashes: txHash ? [txHash] : []
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Handle a sell: calculate realized PnL and remove position if fully sold.
|
|
431
|
+
* Returns the realized PnL in USD.
|
|
432
|
+
*/
|
|
433
|
+
handleSell(token, saleValueUSD) {
|
|
434
|
+
if (BASE_ASSETS.has(token)) return 0;
|
|
435
|
+
const position = this.positions[token];
|
|
436
|
+
if (!position || position.averageEntryPrice <= 0) return 0;
|
|
437
|
+
const estimatedUnitsSold = position.currentAmount > 0 ? Math.min(position.currentAmount, saleValueUSD / position.averageEntryPrice) : saleValueUSD / position.averageEntryPrice;
|
|
438
|
+
const costBasisOfSold = estimatedUnitsSold * position.averageEntryPrice;
|
|
439
|
+
const realizedPnL = saleValueUSD - costBasisOfSold;
|
|
440
|
+
position.totalAmountAcquired = Math.max(0, position.totalAmountAcquired - estimatedUnitsSold);
|
|
441
|
+
position.totalCostBasis = Math.max(0, position.totalCostBasis - costBasisOfSold);
|
|
442
|
+
position.lastUpdateTimestamp = Date.now();
|
|
443
|
+
return realizedPnL;
|
|
444
|
+
}
|
|
445
|
+
// ============================================================
|
|
446
|
+
// BALANCE SYNC (called by runtime each cycle)
|
|
447
|
+
// ============================================================
|
|
448
|
+
/**
|
|
449
|
+
* Sync tracked positions with on-chain balances.
|
|
450
|
+
* Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
|
|
451
|
+
*/
|
|
452
|
+
syncBalances(balances, prices) {
|
|
453
|
+
let changed = false;
|
|
454
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
455
|
+
const token = address.toLowerCase();
|
|
456
|
+
if (BASE_ASSETS.has(token)) continue;
|
|
457
|
+
const decimals = getTokenDecimals(token);
|
|
458
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
459
|
+
if (amount > 0) {
|
|
460
|
+
if (this.positions[token]) {
|
|
461
|
+
if (this.positions[token].currentAmount !== amount) {
|
|
462
|
+
this.positions[token].currentAmount = amount;
|
|
463
|
+
this.positions[token].lastUpdateTimestamp = Date.now();
|
|
464
|
+
changed = true;
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
const price = prices[token] || 0;
|
|
468
|
+
this.positions[token] = {
|
|
469
|
+
token,
|
|
470
|
+
symbol: TOKEN_SYMBOLS[token],
|
|
471
|
+
entryPrice: price,
|
|
472
|
+
averageEntryPrice: price,
|
|
473
|
+
totalCostBasis: amount * price,
|
|
474
|
+
totalAmountAcquired: amount,
|
|
475
|
+
currentAmount: amount,
|
|
476
|
+
entryTimestamp: Date.now(),
|
|
477
|
+
lastUpdateTimestamp: Date.now(),
|
|
478
|
+
txHashes: []
|
|
479
|
+
};
|
|
480
|
+
if (price > 0) {
|
|
481
|
+
console.log(`Position tracker: detected new holding ${TOKEN_SYMBOLS[token] || token.slice(0, 10)} (${amount.toFixed(4)} units @ $${price.toFixed(4)})`);
|
|
482
|
+
}
|
|
483
|
+
changed = true;
|
|
484
|
+
}
|
|
485
|
+
} else if (this.positions[token]) {
|
|
486
|
+
delete this.positions[token];
|
|
487
|
+
changed = true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (changed) {
|
|
491
|
+
this.persist();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// ============================================================
|
|
495
|
+
// QUERY METHODS (for strategies)
|
|
496
|
+
// ============================================================
|
|
497
|
+
/** Get all tracked positions */
|
|
498
|
+
getPositions() {
|
|
499
|
+
return Object.values(this.positions);
|
|
500
|
+
}
|
|
501
|
+
/** Get a single position by token address */
|
|
502
|
+
getPosition(token) {
|
|
503
|
+
return this.positions[token.toLowerCase()];
|
|
504
|
+
}
|
|
505
|
+
/** Get trade history (newest first) */
|
|
506
|
+
getTradeHistory(limit) {
|
|
507
|
+
return limit ? this.tradeHistory.slice(0, limit) : [...this.tradeHistory];
|
|
508
|
+
}
|
|
509
|
+
/** Get unrealized PnL per position given current prices */
|
|
510
|
+
getUnrealizedPnL(prices) {
|
|
511
|
+
const pnl = {};
|
|
512
|
+
for (const pos of Object.values(this.positions)) {
|
|
513
|
+
const currentPrice = prices[pos.token] || 0;
|
|
514
|
+
if (currentPrice > 0 && pos.averageEntryPrice > 0 && pos.currentAmount > 0) {
|
|
515
|
+
const currentValue = pos.currentAmount * currentPrice;
|
|
516
|
+
const costBasis = pos.currentAmount * pos.averageEntryPrice;
|
|
517
|
+
pnl[pos.token] = currentValue - costBasis;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return pnl;
|
|
521
|
+
}
|
|
522
|
+
/** Get total unrealized PnL across all positions */
|
|
523
|
+
getTotalUnrealizedPnL(prices) {
|
|
524
|
+
const pnl = this.getUnrealizedPnL(prices);
|
|
525
|
+
return Object.values(pnl).reduce((sum, v) => sum + v, 0);
|
|
526
|
+
}
|
|
527
|
+
// ============================================================
|
|
528
|
+
// RISK STATE PERSISTENCE
|
|
529
|
+
// ============================================================
|
|
530
|
+
/** Load persisted risk state */
|
|
531
|
+
getRiskState() {
|
|
532
|
+
return this.store.get(KEY_RISK_STATE) || {
|
|
533
|
+
dailyPnL: 0,
|
|
534
|
+
dailyFees: 0,
|
|
535
|
+
lastResetDate: ""
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/** Save risk state to persistent store */
|
|
539
|
+
saveRiskState(state) {
|
|
540
|
+
this.store.set(KEY_RISK_STATE, state);
|
|
541
|
+
}
|
|
542
|
+
// ============================================================
|
|
543
|
+
// RELAY SUMMARY
|
|
544
|
+
// ============================================================
|
|
545
|
+
/** Get a compact summary for relay heartbeats */
|
|
546
|
+
getPositionSummary(prices) {
|
|
547
|
+
const unrealizedPnL = this.getUnrealizedPnL(prices);
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
const topPositions = Object.values(this.positions).map((pos) => ({
|
|
550
|
+
token: pos.token,
|
|
551
|
+
symbol: pos.symbol,
|
|
552
|
+
unrealizedPnL: unrealizedPnL[pos.token] || 0,
|
|
553
|
+
holdingDuration: now - pos.entryTimestamp
|
|
554
|
+
})).sort((a, b) => Math.abs(b.unrealizedPnL) - Math.abs(a.unrealizedPnL)).slice(0, 5);
|
|
555
|
+
const oneDayAgo = now - 24 * 60 * 60 * 1e3;
|
|
556
|
+
const recentTrades = this.tradeHistory.filter((t) => t.timestamp > oneDayAgo).length;
|
|
557
|
+
const totalRealizedPnL = this.tradeHistory.filter((t) => t.realizedPnL !== void 0).reduce((sum, t) => sum + (t.realizedPnL || 0), 0);
|
|
558
|
+
return {
|
|
559
|
+
openPositions: Object.keys(this.positions).length,
|
|
560
|
+
totalUnrealizedPnL: Object.values(unrealizedPnL).reduce((s, v) => s + v, 0),
|
|
561
|
+
topPositions,
|
|
562
|
+
recentTrades,
|
|
563
|
+
totalRealizedPnL
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// ============================================================
|
|
567
|
+
// INTERNAL
|
|
568
|
+
// ============================================================
|
|
569
|
+
persist() {
|
|
570
|
+
this.store.set(KEY_POSITIONS, this.positions);
|
|
571
|
+
this.store.set(KEY_TRADE_HISTORY, this.tradeHistory);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/llm/openai.ts
|
|
576
|
+
var import_openai = __toESM(require("openai"));
|
|
577
|
+
|
|
578
|
+
// src/llm/base.ts
|
|
579
|
+
var BaseLLMAdapter = class {
|
|
580
|
+
config;
|
|
581
|
+
constructor(config) {
|
|
582
|
+
this.config = config;
|
|
583
|
+
}
|
|
584
|
+
getMetadata() {
|
|
585
|
+
return {
|
|
586
|
+
provider: this.config.provider,
|
|
587
|
+
model: this.config.model || "unknown",
|
|
588
|
+
isLocal: this.config.provider === "ollama"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Format model name for display
|
|
593
|
+
*/
|
|
594
|
+
getDisplayModel() {
|
|
595
|
+
if (this.config.provider === "ollama") {
|
|
596
|
+
return `Local (${this.config.model || "ollama"})`;
|
|
597
|
+
}
|
|
598
|
+
return this.config.model || this.config.provider;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// src/llm/openai.ts
|
|
603
|
+
var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
604
|
+
client;
|
|
605
|
+
constructor(config) {
|
|
606
|
+
super(config);
|
|
607
|
+
if (!config.apiKey && !config.endpoint) {
|
|
608
|
+
throw new Error("OpenAI API key or custom endpoint required");
|
|
609
|
+
}
|
|
610
|
+
this.client = new import_openai.default({
|
|
611
|
+
apiKey: config.apiKey || "not-needed-for-custom",
|
|
612
|
+
baseURL: config.endpoint
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async chat(messages) {
|
|
616
|
+
try {
|
|
617
|
+
const response = await this.client.chat.completions.create({
|
|
618
|
+
model: this.config.model || "gpt-4.1",
|
|
619
|
+
messages: messages.map((m) => ({
|
|
620
|
+
role: m.role,
|
|
621
|
+
content: m.content
|
|
622
|
+
})),
|
|
623
|
+
temperature: this.config.temperature,
|
|
624
|
+
max_tokens: this.config.maxTokens
|
|
625
|
+
});
|
|
626
|
+
const choice = response.choices[0];
|
|
627
|
+
if (!choice || !choice.message) {
|
|
628
|
+
throw new Error("No response from OpenAI");
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
content: choice.message.content || "",
|
|
632
|
+
usage: response.usage ? {
|
|
633
|
+
promptTokens: response.usage.prompt_tokens,
|
|
634
|
+
completionTokens: response.usage.completion_tokens,
|
|
635
|
+
totalTokens: response.usage.total_tokens
|
|
636
|
+
} : void 0
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error instanceof import_openai.default.APIError) {
|
|
640
|
+
throw new Error(`OpenAI API error: ${error.message}`);
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/llm/anthropic.ts
|
|
648
|
+
var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
649
|
+
apiKey;
|
|
650
|
+
baseUrl;
|
|
651
|
+
constructor(config) {
|
|
652
|
+
super(config);
|
|
653
|
+
if (!config.apiKey) {
|
|
654
|
+
throw new Error("Anthropic API key required");
|
|
655
|
+
}
|
|
656
|
+
this.apiKey = config.apiKey;
|
|
657
|
+
this.baseUrl = config.endpoint || "https://api.anthropic.com";
|
|
658
|
+
}
|
|
659
|
+
async chat(messages) {
|
|
660
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
661
|
+
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
662
|
+
const body = {
|
|
663
|
+
model: this.config.model || "claude-opus-4-5-20251101",
|
|
664
|
+
max_tokens: this.config.maxTokens || 4096,
|
|
665
|
+
temperature: this.config.temperature,
|
|
666
|
+
system: systemMessage?.content,
|
|
667
|
+
messages: chatMessages.map((m) => ({
|
|
668
|
+
role: m.role,
|
|
133
669
|
content: m.content
|
|
134
670
|
}))
|
|
135
671
|
};
|
|
@@ -1150,349 +1686,76 @@ function classifyTradeError(message) {
|
|
|
1150
1686
|
};
|
|
1151
1687
|
}
|
|
1152
1688
|
|
|
1153
|
-
// src/trading/
|
|
1154
|
-
var
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
"
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
1165
|
-
// DAI
|
|
1166
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
1167
|
-
// cbETH
|
|
1168
|
-
[NATIVE_ETH.toLowerCase()]: 18,
|
|
1169
|
-
// Native ETH
|
|
1170
|
-
// Base Mainnet — Established tokens
|
|
1171
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
1172
|
-
// AERO (Aerodrome)
|
|
1173
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
1174
|
-
// BRETT
|
|
1175
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
1176
|
-
// DEGEN
|
|
1177
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
1178
|
-
// VIRTUAL
|
|
1179
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
1180
|
-
// TOSHI
|
|
1181
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
1182
|
-
// cbBTC
|
|
1183
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
1184
|
-
// ezETH (Renzo)
|
|
1185
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
1186
|
-
// wstETH (Lido)
|
|
1187
|
-
};
|
|
1188
|
-
function getTokenDecimals(address) {
|
|
1189
|
-
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
1190
|
-
if (decimals === void 0) {
|
|
1191
|
-
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
1192
|
-
return 18;
|
|
1193
|
-
}
|
|
1194
|
-
return decimals;
|
|
1195
|
-
}
|
|
1196
|
-
var TOKEN_TO_COINGECKO = {
|
|
1197
|
-
// Core
|
|
1198
|
-
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
1199
|
-
// WETH
|
|
1200
|
-
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
1201
|
-
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
1202
|
-
// USDC
|
|
1203
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
1204
|
-
// USDbC
|
|
1205
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
1206
|
-
// cbETH
|
|
1207
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
1208
|
-
// DAI
|
|
1209
|
-
// Established
|
|
1210
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
1211
|
-
// AERO
|
|
1212
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
1213
|
-
// BRETT
|
|
1214
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
1215
|
-
// DEGEN
|
|
1216
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
1217
|
-
// VIRTUAL
|
|
1218
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
1219
|
-
// TOSHI
|
|
1220
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
1221
|
-
// cbBTC
|
|
1222
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
1223
|
-
// ezETH
|
|
1224
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
1225
|
-
// wstETH
|
|
1226
|
-
};
|
|
1227
|
-
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
1228
|
-
var PRICE_STALENESS_MS = 6e4;
|
|
1229
|
-
var MarketDataService = class {
|
|
1230
|
-
rpcUrl;
|
|
1231
|
-
client;
|
|
1232
|
-
/** Cached prices from last fetch */
|
|
1233
|
-
cachedPrices = {};
|
|
1234
|
-
/** Timestamp of last successful price fetch */
|
|
1235
|
-
lastPriceFetchAt = 0;
|
|
1236
|
-
constructor(rpcUrl) {
|
|
1237
|
-
this.rpcUrl = rpcUrl;
|
|
1238
|
-
this.client = (0, import_viem.createPublicClient)({
|
|
1239
|
-
transport: (0, import_viem.http)(rpcUrl)
|
|
1240
|
-
});
|
|
1689
|
+
// src/trading/risk.ts
|
|
1690
|
+
var RiskManager = class {
|
|
1691
|
+
config;
|
|
1692
|
+
dailyPnL = 0;
|
|
1693
|
+
dailyFees = 0;
|
|
1694
|
+
lastResetDate = "";
|
|
1695
|
+
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1696
|
+
minTradeValueUSD;
|
|
1697
|
+
constructor(config) {
|
|
1698
|
+
this.config = config;
|
|
1699
|
+
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1241
1700
|
}
|
|
1242
|
-
/** Cached volume data */
|
|
1243
|
-
cachedVolume24h = {};
|
|
1244
|
-
/** Cached price change data */
|
|
1245
|
-
cachedPriceChange24h = {};
|
|
1246
1701
|
/**
|
|
1247
|
-
*
|
|
1702
|
+
* Filter signals through risk checks
|
|
1703
|
+
* Returns only signals that pass all guardrails
|
|
1248
1704
|
*/
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
gasPrice = await this.client.getGasPrice();
|
|
1256
|
-
} catch {
|
|
1705
|
+
filterSignals(signals, marketData) {
|
|
1706
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1707
|
+
if (today !== this.lastResetDate) {
|
|
1708
|
+
this.dailyPnL = 0;
|
|
1709
|
+
this.dailyFees = 0;
|
|
1710
|
+
this.lastResetDate = today;
|
|
1257
1711
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1264
|
-
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1265
|
-
gasPrice,
|
|
1266
|
-
network: {
|
|
1267
|
-
chainId: this.client.chain?.id ?? 8453
|
|
1268
|
-
}
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Check if cached prices are still fresh
|
|
1273
|
-
*/
|
|
1274
|
-
get pricesAreFresh() {
|
|
1275
|
-
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
1712
|
+
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1713
|
+
console.warn("Daily loss limit reached - no new trades");
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1276
1717
|
}
|
|
1277
1718
|
/**
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1719
|
+
* Validate individual signal against risk limits.
|
|
1720
|
+
* Sell signals are exempt from position size and minimum value checks —
|
|
1721
|
+
* those guardrails prevent oversized/dust buys, but blocking exits traps capital.
|
|
1280
1722
|
*/
|
|
1281
|
-
|
|
1282
|
-
if (
|
|
1283
|
-
|
|
1284
|
-
for (const addr of tokenAddresses) {
|
|
1285
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1286
|
-
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1287
|
-
prices2[addr.toLowerCase()] = 1;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
return prices2;
|
|
1723
|
+
validateSignal(signal, marketData) {
|
|
1724
|
+
if (signal.action === "hold") {
|
|
1725
|
+
return true;
|
|
1291
1726
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1296
|
-
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1297
|
-
idsToFetch.add(cgId);
|
|
1298
|
-
}
|
|
1727
|
+
if (signal.confidence < 0.5) {
|
|
1728
|
+
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1729
|
+
return false;
|
|
1299
1730
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
try {
|
|
1303
|
-
const ids = Array.from(idsToFetch).join(",");
|
|
1304
|
-
const response = await fetch(
|
|
1305
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1306
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1307
|
-
);
|
|
1308
|
-
if (response.ok) {
|
|
1309
|
-
const data = await response.json();
|
|
1310
|
-
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1311
|
-
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1312
|
-
if (id === cgId) {
|
|
1313
|
-
const key = addr.toLowerCase();
|
|
1314
|
-
prices[key] = priceData.usd;
|
|
1315
|
-
if (priceData.usd_24h_vol !== void 0) {
|
|
1316
|
-
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1317
|
-
}
|
|
1318
|
-
if (priceData.usd_24h_change !== void 0) {
|
|
1319
|
-
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
this.lastPriceFetchAt = Date.now();
|
|
1325
|
-
} else {
|
|
1326
|
-
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1327
|
-
}
|
|
1328
|
-
} catch (error) {
|
|
1329
|
-
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1330
|
-
}
|
|
1731
|
+
if (signal.action === "sell") {
|
|
1732
|
+
return true;
|
|
1331
1733
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1734
|
+
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1735
|
+
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1736
|
+
if (signalValue > maxPositionValue) {
|
|
1737
|
+
console.warn(
|
|
1738
|
+
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1739
|
+
);
|
|
1740
|
+
return false;
|
|
1337
1741
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1344
|
-
const llamaResponse = await fetch(
|
|
1345
|
-
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1346
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1742
|
+
if (this.config.maxConcurrentPositions) {
|
|
1743
|
+
const activePositions = this.countActivePositions(marketData);
|
|
1744
|
+
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1745
|
+
console.warn(
|
|
1746
|
+
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1347
1747
|
);
|
|
1348
|
-
|
|
1349
|
-
const llamaData = await llamaResponse.json();
|
|
1350
|
-
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1351
|
-
const addr = key.replace("base:", "").toLowerCase();
|
|
1352
|
-
if (data.price && data.confidence > 0.5) {
|
|
1353
|
-
prices[addr] = data.price;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1357
|
-
}
|
|
1358
|
-
} catch {
|
|
1748
|
+
return false;
|
|
1359
1749
|
}
|
|
1360
1750
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
this.
|
|
1363
|
-
|
|
1364
|
-
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1365
|
-
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1366
|
-
return { ...this.cachedPrices };
|
|
1367
|
-
}
|
|
1368
|
-
for (const addr of tokenAddresses) {
|
|
1369
|
-
if (!prices[addr.toLowerCase()]) {
|
|
1370
|
-
console.warn(`No price available for ${addr}, using 0`);
|
|
1371
|
-
prices[addr.toLowerCase()] = 0;
|
|
1372
|
-
}
|
|
1751
|
+
if (signalValue < this.minTradeValueUSD) {
|
|
1752
|
+
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1753
|
+
return false;
|
|
1373
1754
|
}
|
|
1374
|
-
return
|
|
1755
|
+
return true;
|
|
1375
1756
|
}
|
|
1376
1757
|
/**
|
|
1377
|
-
*
|
|
1378
|
-
*/
|
|
1379
|
-
async fetchBalances(walletAddress, tokenAddresses) {
|
|
1380
|
-
const balances = {};
|
|
1381
|
-
const wallet = walletAddress;
|
|
1382
|
-
try {
|
|
1383
|
-
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
1384
|
-
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
1385
|
-
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
1386
|
-
try {
|
|
1387
|
-
const balance = await this.client.readContract({
|
|
1388
|
-
address: tokenAddress,
|
|
1389
|
-
abi: import_viem.erc20Abi,
|
|
1390
|
-
functionName: "balanceOf",
|
|
1391
|
-
args: [wallet]
|
|
1392
|
-
});
|
|
1393
|
-
return { address: tokenAddress.toLowerCase(), balance };
|
|
1394
|
-
} catch (error) {
|
|
1395
|
-
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
1396
|
-
}
|
|
1397
|
-
});
|
|
1398
|
-
const results = await Promise.all(erc20Promises);
|
|
1399
|
-
for (const { address, balance } of results) {
|
|
1400
|
-
balances[address] = balance;
|
|
1401
|
-
}
|
|
1402
|
-
} catch (error) {
|
|
1403
|
-
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
1404
|
-
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
1405
|
-
for (const address of tokenAddresses) {
|
|
1406
|
-
balances[address.toLowerCase()] = 0n;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
return balances;
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Calculate total portfolio value in USD
|
|
1413
|
-
*/
|
|
1414
|
-
calculatePortfolioValue(balances, prices) {
|
|
1415
|
-
let total = 0;
|
|
1416
|
-
for (const [address, balance] of Object.entries(balances)) {
|
|
1417
|
-
const price = prices[address.toLowerCase()] || 0;
|
|
1418
|
-
const decimals = getTokenDecimals(address);
|
|
1419
|
-
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1420
|
-
total += amount * price;
|
|
1421
|
-
}
|
|
1422
|
-
return total;
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
1425
|
-
|
|
1426
|
-
// src/trading/risk.ts
|
|
1427
|
-
var RiskManager = class {
|
|
1428
|
-
config;
|
|
1429
|
-
dailyPnL = 0;
|
|
1430
|
-
dailyFees = 0;
|
|
1431
|
-
lastResetDate = "";
|
|
1432
|
-
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1433
|
-
minTradeValueUSD;
|
|
1434
|
-
constructor(config) {
|
|
1435
|
-
this.config = config;
|
|
1436
|
-
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Filter signals through risk checks
|
|
1440
|
-
* Returns only signals that pass all guardrails
|
|
1441
|
-
*/
|
|
1442
|
-
filterSignals(signals, marketData) {
|
|
1443
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1444
|
-
if (today !== this.lastResetDate) {
|
|
1445
|
-
this.dailyPnL = 0;
|
|
1446
|
-
this.dailyFees = 0;
|
|
1447
|
-
this.lastResetDate = today;
|
|
1448
|
-
}
|
|
1449
|
-
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1450
|
-
console.warn("Daily loss limit reached - no new trades");
|
|
1451
|
-
return [];
|
|
1452
|
-
}
|
|
1453
|
-
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* Validate individual signal against risk limits.
|
|
1457
|
-
* Sell signals are exempt from position size and minimum value checks —
|
|
1458
|
-
* those guardrails prevent oversized/dust buys, but blocking exits traps capital.
|
|
1459
|
-
*/
|
|
1460
|
-
validateSignal(signal, marketData) {
|
|
1461
|
-
if (signal.action === "hold") {
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1464
|
-
if (signal.confidence < 0.5) {
|
|
1465
|
-
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1466
|
-
return false;
|
|
1467
|
-
}
|
|
1468
|
-
if (signal.action === "sell") {
|
|
1469
|
-
return true;
|
|
1470
|
-
}
|
|
1471
|
-
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1472
|
-
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1473
|
-
if (signalValue > maxPositionValue) {
|
|
1474
|
-
console.warn(
|
|
1475
|
-
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1476
|
-
);
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
if (this.config.maxConcurrentPositions) {
|
|
1480
|
-
const activePositions = this.countActivePositions(marketData);
|
|
1481
|
-
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1482
|
-
console.warn(
|
|
1483
|
-
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1484
|
-
);
|
|
1485
|
-
return false;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
if (signalValue < this.minTradeValueUSD) {
|
|
1489
|
-
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1490
|
-
return false;
|
|
1491
|
-
}
|
|
1492
|
-
return true;
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1758
|
+
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1496
1759
|
*/
|
|
1497
1760
|
countActivePositions(marketData) {
|
|
1498
1761
|
const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
@@ -1533,6 +1796,31 @@ var RiskManager = class {
|
|
|
1533
1796
|
updateFees(fees) {
|
|
1534
1797
|
this.dailyFees += fees;
|
|
1535
1798
|
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Export current risk state for persistence across restarts.
|
|
1801
|
+
*/
|
|
1802
|
+
exportState() {
|
|
1803
|
+
return {
|
|
1804
|
+
dailyPnL: this.dailyPnL,
|
|
1805
|
+
dailyFees: this.dailyFees,
|
|
1806
|
+
lastResetDate: this.lastResetDate
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Restore risk state from persistence (called on startup).
|
|
1811
|
+
* Only restores if the saved state is from today — expired state is ignored.
|
|
1812
|
+
*/
|
|
1813
|
+
restoreState(state) {
|
|
1814
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1815
|
+
if (state.lastResetDate === today) {
|
|
1816
|
+
this.dailyPnL = state.dailyPnL;
|
|
1817
|
+
this.dailyFees = state.dailyFees;
|
|
1818
|
+
this.lastResetDate = state.lastResetDate;
|
|
1819
|
+
console.log(`Risk state restored: PnL=$${this.dailyPnL.toFixed(2)}, Fees=$${this.dailyFees.toFixed(2)}`);
|
|
1820
|
+
} else {
|
|
1821
|
+
console.log("Risk state expired (different day), starting fresh");
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1536
1824
|
/**
|
|
1537
1825
|
* Get current risk status
|
|
1538
1826
|
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
@@ -1994,300 +2282,37 @@ var VaultManager = class {
|
|
|
1994
2282
|
};
|
|
1995
2283
|
|
|
1996
2284
|
// src/relay.ts
|
|
1997
|
-
var
|
|
1998
|
-
var
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
reconnectAttempts = 0;
|
|
2006
|
-
maxReconnectAttempts = 50;
|
|
2007
|
-
reconnectTimer = null;
|
|
2008
|
-
heartbeatTimer = null;
|
|
2009
|
-
stopped = false;
|
|
2285
|
+
var import_ws2 = __toESM(require("ws"));
|
|
2286
|
+
var import_accounts4 = require("viem/accounts");
|
|
2287
|
+
|
|
2288
|
+
// src/perp/client.ts
|
|
2289
|
+
var HyperliquidClient = class {
|
|
2290
|
+
apiUrl;
|
|
2291
|
+
meta = null;
|
|
2292
|
+
assetIndexCache = /* @__PURE__ */ new Map();
|
|
2010
2293
|
constructor(config) {
|
|
2011
|
-
this.
|
|
2294
|
+
this.apiUrl = config.apiUrl;
|
|
2012
2295
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
console.error("Relay: Failed to create WebSocket:", error);
|
|
2024
|
-
this.scheduleReconnect();
|
|
2025
|
-
reject(error);
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
const connectTimeout = setTimeout(() => {
|
|
2029
|
-
if (!this.authenticated) {
|
|
2030
|
-
console.error("Relay: Connection timeout");
|
|
2031
|
-
this.ws?.close();
|
|
2032
|
-
this.scheduleReconnect();
|
|
2033
|
-
reject(new Error("Connection timeout"));
|
|
2034
|
-
}
|
|
2035
|
-
}, 15e3);
|
|
2036
|
-
this.ws.on("open", async () => {
|
|
2037
|
-
this.authRejected = false;
|
|
2038
|
-
console.log("Relay: Connected, authenticating...");
|
|
2039
|
-
try {
|
|
2040
|
-
await this.authenticate();
|
|
2041
|
-
} catch (error) {
|
|
2042
|
-
console.error("Relay: Authentication failed:", error);
|
|
2043
|
-
this.ws?.close();
|
|
2044
|
-
clearTimeout(connectTimeout);
|
|
2045
|
-
reject(error);
|
|
2046
|
-
}
|
|
2047
|
-
});
|
|
2048
|
-
this.ws.on("message", (raw) => {
|
|
2049
|
-
try {
|
|
2050
|
-
const data = JSON.parse(raw.toString());
|
|
2051
|
-
this.handleMessage(data);
|
|
2052
|
-
if (data.type === "auth_success") {
|
|
2053
|
-
clearTimeout(connectTimeout);
|
|
2054
|
-
this.authenticated = true;
|
|
2055
|
-
this.reconnectAttempts = 0;
|
|
2056
|
-
this.startHeartbeat();
|
|
2057
|
-
console.log("Relay: Authenticated successfully");
|
|
2058
|
-
resolve();
|
|
2059
|
-
} else if (data.type === "auth_error") {
|
|
2060
|
-
clearTimeout(connectTimeout);
|
|
2061
|
-
this.authRejected = true;
|
|
2062
|
-
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
2063
|
-
reject(new Error(data.message));
|
|
2064
|
-
}
|
|
2065
|
-
} catch {
|
|
2066
|
-
}
|
|
2067
|
-
});
|
|
2068
|
-
this.ws.on("close", (code, reason) => {
|
|
2069
|
-
clearTimeout(connectTimeout);
|
|
2070
|
-
this.authenticated = false;
|
|
2071
|
-
this.stopHeartbeat();
|
|
2072
|
-
if (!this.stopped) {
|
|
2073
|
-
if (!this.authRejected) {
|
|
2074
|
-
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
2075
|
-
}
|
|
2076
|
-
this.scheduleReconnect();
|
|
2077
|
-
}
|
|
2078
|
-
});
|
|
2079
|
-
this.ws.on("error", (error) => {
|
|
2080
|
-
if (!this.stopped) {
|
|
2081
|
-
console.error("Relay: WebSocket error:", error.message);
|
|
2082
|
-
}
|
|
2083
|
-
});
|
|
2296
|
+
// ============================================================
|
|
2297
|
+
// INFO API (read-only)
|
|
2298
|
+
// ============================================================
|
|
2299
|
+
/** Fetch perpetuals metadata (asset specs, names, indices) */
|
|
2300
|
+
async getMeta() {
|
|
2301
|
+
if (this.meta) return this.meta;
|
|
2302
|
+
const resp = await this.infoRequest({ type: "meta" });
|
|
2303
|
+
this.meta = resp.universe;
|
|
2304
|
+
this.meta.forEach((asset, idx) => {
|
|
2305
|
+
this.assetIndexCache.set(asset.name, idx);
|
|
2084
2306
|
});
|
|
2307
|
+
return this.meta;
|
|
2085
2308
|
}
|
|
2086
|
-
/**
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
const signature = await (0, import_accounts2.signMessage)({
|
|
2094
|
-
message,
|
|
2095
|
-
privateKey: this.config.privateKey
|
|
2096
|
-
});
|
|
2097
|
-
this.send({
|
|
2098
|
-
type: "auth",
|
|
2099
|
-
agentId: this.config.agentId,
|
|
2100
|
-
wallet: account.address,
|
|
2101
|
-
timestamp,
|
|
2102
|
-
signature,
|
|
2103
|
-
sdkVersion: import_sdk.SDK_VERSION
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
/**
|
|
2107
|
-
* Handle incoming messages from the relay server
|
|
2108
|
-
*/
|
|
2109
|
-
handleMessage(data) {
|
|
2110
|
-
switch (data.type) {
|
|
2111
|
-
case "command":
|
|
2112
|
-
if (data.command && this.config.onCommand) {
|
|
2113
|
-
this.config.onCommand(data.command);
|
|
2114
|
-
}
|
|
2115
|
-
break;
|
|
2116
|
-
case "auth_success":
|
|
2117
|
-
case "auth_error":
|
|
2118
|
-
break;
|
|
2119
|
-
case "error":
|
|
2120
|
-
console.error(`Relay: Server error: ${data.message}`);
|
|
2121
|
-
break;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
/**
|
|
2125
|
-
* Send a status heartbeat
|
|
2126
|
-
*/
|
|
2127
|
-
sendHeartbeat(status) {
|
|
2128
|
-
if (!this.authenticated) return;
|
|
2129
|
-
this.send({
|
|
2130
|
-
type: "heartbeat",
|
|
2131
|
-
agentId: this.config.agentId,
|
|
2132
|
-
status
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
/**
|
|
2136
|
-
* Send a status update (outside of regular heartbeat)
|
|
2137
|
-
*/
|
|
2138
|
-
sendStatusUpdate(status) {
|
|
2139
|
-
if (!this.authenticated) return;
|
|
2140
|
-
this.send({
|
|
2141
|
-
type: "status_update",
|
|
2142
|
-
agentId: this.config.agentId,
|
|
2143
|
-
status
|
|
2144
|
-
});
|
|
2145
|
-
}
|
|
2146
|
-
/**
|
|
2147
|
-
* Send a message to the command center
|
|
2148
|
-
*/
|
|
2149
|
-
sendMessage(messageType, level, title, body, data) {
|
|
2150
|
-
if (!this.authenticated) return;
|
|
2151
|
-
this.send({
|
|
2152
|
-
type: "message",
|
|
2153
|
-
agentId: this.config.agentId,
|
|
2154
|
-
messageType,
|
|
2155
|
-
level,
|
|
2156
|
-
title,
|
|
2157
|
-
body,
|
|
2158
|
-
data
|
|
2159
|
-
});
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Send a command execution result
|
|
2163
|
-
*/
|
|
2164
|
-
sendCommandResult(commandId, success, result) {
|
|
2165
|
-
if (!this.authenticated) return;
|
|
2166
|
-
this.send({
|
|
2167
|
-
type: "command_result",
|
|
2168
|
-
agentId: this.config.agentId,
|
|
2169
|
-
commandId,
|
|
2170
|
-
success,
|
|
2171
|
-
result
|
|
2172
|
-
});
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Start the heartbeat timer
|
|
2176
|
-
*/
|
|
2177
|
-
startHeartbeat() {
|
|
2178
|
-
this.stopHeartbeat();
|
|
2179
|
-
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
2180
|
-
this.heartbeatTimer = setInterval(() => {
|
|
2181
|
-
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2182
|
-
this.ws.ping();
|
|
2183
|
-
}
|
|
2184
|
-
}, interval);
|
|
2185
|
-
}
|
|
2186
|
-
/**
|
|
2187
|
-
* Stop the heartbeat timer
|
|
2188
|
-
*/
|
|
2189
|
-
stopHeartbeat() {
|
|
2190
|
-
if (this.heartbeatTimer) {
|
|
2191
|
-
clearInterval(this.heartbeatTimer);
|
|
2192
|
-
this.heartbeatTimer = null;
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
/**
|
|
2196
|
-
* Schedule a reconnection with exponential backoff
|
|
2197
|
-
*/
|
|
2198
|
-
scheduleReconnect() {
|
|
2199
|
-
if (this.stopped) return;
|
|
2200
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
2201
|
-
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
2202
|
-
return;
|
|
2203
|
-
}
|
|
2204
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
2205
|
-
this.reconnectAttempts++;
|
|
2206
|
-
console.log(
|
|
2207
|
-
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
2208
|
-
);
|
|
2209
|
-
this.reconnectTimer = setTimeout(() => {
|
|
2210
|
-
this.connect().catch(() => {
|
|
2211
|
-
});
|
|
2212
|
-
}, delay);
|
|
2213
|
-
}
|
|
2214
|
-
/**
|
|
2215
|
-
* Send a JSON message to the WebSocket
|
|
2216
|
-
*/
|
|
2217
|
-
send(data) {
|
|
2218
|
-
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2219
|
-
this.ws.send(JSON.stringify(data));
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* Check if connected and authenticated
|
|
2224
|
-
*/
|
|
2225
|
-
get isConnected() {
|
|
2226
|
-
return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
|
|
2227
|
-
}
|
|
2228
|
-
/**
|
|
2229
|
-
* Disconnect and stop reconnecting
|
|
2230
|
-
*/
|
|
2231
|
-
disconnect() {
|
|
2232
|
-
this.stopped = true;
|
|
2233
|
-
this.stopHeartbeat();
|
|
2234
|
-
if (this.reconnectTimer) {
|
|
2235
|
-
clearTimeout(this.reconnectTimer);
|
|
2236
|
-
this.reconnectTimer = null;
|
|
2237
|
-
}
|
|
2238
|
-
if (this.ws) {
|
|
2239
|
-
this.ws.close(1e3, "Agent shutting down");
|
|
2240
|
-
this.ws = null;
|
|
2241
|
-
}
|
|
2242
|
-
this.authenticated = false;
|
|
2243
|
-
console.log("Relay: Disconnected");
|
|
2244
|
-
}
|
|
2245
|
-
};
|
|
2246
|
-
|
|
2247
|
-
// src/browser-open.ts
|
|
2248
|
-
var import_child_process2 = require("child_process");
|
|
2249
|
-
function openBrowser(url) {
|
|
2250
|
-
const platform = process.platform;
|
|
2251
|
-
try {
|
|
2252
|
-
if (platform === "darwin") {
|
|
2253
|
-
(0, import_child_process2.exec)(`open "${url}"`);
|
|
2254
|
-
} else if (platform === "win32") {
|
|
2255
|
-
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
2256
|
-
} else {
|
|
2257
|
-
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
2258
|
-
}
|
|
2259
|
-
} catch {
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
// src/perp/client.ts
|
|
2264
|
-
var HyperliquidClient = class {
|
|
2265
|
-
apiUrl;
|
|
2266
|
-
meta = null;
|
|
2267
|
-
assetIndexCache = /* @__PURE__ */ new Map();
|
|
2268
|
-
constructor(config) {
|
|
2269
|
-
this.apiUrl = config.apiUrl;
|
|
2270
|
-
}
|
|
2271
|
-
// ============================================================
|
|
2272
|
-
// INFO API (read-only)
|
|
2273
|
-
// ============================================================
|
|
2274
|
-
/** Fetch perpetuals metadata (asset specs, names, indices) */
|
|
2275
|
-
async getMeta() {
|
|
2276
|
-
if (this.meta) return this.meta;
|
|
2277
|
-
const resp = await this.infoRequest({ type: "meta" });
|
|
2278
|
-
this.meta = resp.universe;
|
|
2279
|
-
this.meta.forEach((asset, idx) => {
|
|
2280
|
-
this.assetIndexCache.set(asset.name, idx);
|
|
2281
|
-
});
|
|
2282
|
-
return this.meta;
|
|
2283
|
-
}
|
|
2284
|
-
/** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
|
|
2285
|
-
async getAssetIndex(coin) {
|
|
2286
|
-
if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
|
|
2287
|
-
await this.getMeta();
|
|
2288
|
-
const idx = this.assetIndexCache.get(coin);
|
|
2289
|
-
if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
|
|
2290
|
-
return idx;
|
|
2309
|
+
/** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
|
|
2310
|
+
async getAssetIndex(coin) {
|
|
2311
|
+
if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
|
|
2312
|
+
await this.getMeta();
|
|
2313
|
+
const idx = this.assetIndexCache.get(coin);
|
|
2314
|
+
if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
|
|
2315
|
+
return idx;
|
|
2291
2316
|
}
|
|
2292
2317
|
/** Get mid-market prices for all perpetuals */
|
|
2293
2318
|
async getAllMids() {
|
|
@@ -2864,7 +2889,7 @@ var PositionManager = class {
|
|
|
2864
2889
|
};
|
|
2865
2890
|
|
|
2866
2891
|
// src/perp/websocket.ts
|
|
2867
|
-
var
|
|
2892
|
+
var import_ws = __toESM(require("ws"));
|
|
2868
2893
|
var HyperliquidWebSocket = class {
|
|
2869
2894
|
wsUrl;
|
|
2870
2895
|
userAddress;
|
|
@@ -2896,14 +2921,14 @@ var HyperliquidWebSocket = class {
|
|
|
2896
2921
|
* Connect to Hyperliquid WebSocket and subscribe to user events.
|
|
2897
2922
|
*/
|
|
2898
2923
|
async connect() {
|
|
2899
|
-
if (this.ws?.readyState ===
|
|
2924
|
+
if (this.ws?.readyState === import_ws.default.OPEN || this.isConnecting) {
|
|
2900
2925
|
return;
|
|
2901
2926
|
}
|
|
2902
2927
|
this.isConnecting = true;
|
|
2903
2928
|
this.shouldReconnect = true;
|
|
2904
2929
|
return new Promise((resolve, reject) => {
|
|
2905
2930
|
try {
|
|
2906
|
-
this.ws = new
|
|
2931
|
+
this.ws = new import_ws.default(this.wsUrl);
|
|
2907
2932
|
this.ws.on("open", () => {
|
|
2908
2933
|
this.isConnecting = false;
|
|
2909
2934
|
this.reconnectAttempts = 0;
|
|
@@ -2956,7 +2981,7 @@ var HyperliquidWebSocket = class {
|
|
|
2956
2981
|
this.stopPing();
|
|
2957
2982
|
if (this.ws) {
|
|
2958
2983
|
this.ws.removeAllListeners();
|
|
2959
|
-
if (this.ws.readyState ===
|
|
2984
|
+
if (this.ws.readyState === import_ws.default.OPEN) {
|
|
2960
2985
|
this.ws.close(1e3, "Client disconnect");
|
|
2961
2986
|
}
|
|
2962
2987
|
this.ws = null;
|
|
@@ -2967,7 +2992,7 @@ var HyperliquidWebSocket = class {
|
|
|
2967
2992
|
* Check if WebSocket is connected.
|
|
2968
2993
|
*/
|
|
2969
2994
|
get isConnected() {
|
|
2970
|
-
return this.ws?.readyState ===
|
|
2995
|
+
return this.ws?.readyState === import_ws.default.OPEN;
|
|
2971
2996
|
}
|
|
2972
2997
|
// ============================================================
|
|
2973
2998
|
// EVENT HANDLERS
|
|
@@ -3094,7 +3119,7 @@ var HyperliquidWebSocket = class {
|
|
|
3094
3119
|
startPing() {
|
|
3095
3120
|
this.stopPing();
|
|
3096
3121
|
this.pingTimer = setInterval(() => {
|
|
3097
|
-
if (this.ws?.readyState ===
|
|
3122
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
3098
3123
|
this.ws.send(JSON.stringify({ method: "ping" }));
|
|
3099
3124
|
}
|
|
3100
3125
|
}, 25e3);
|
|
@@ -3109,7 +3134,7 @@ var HyperliquidWebSocket = class {
|
|
|
3109
3134
|
// HELPERS
|
|
3110
3135
|
// ============================================================
|
|
3111
3136
|
subscribe(msg) {
|
|
3112
|
-
if (this.ws?.readyState ===
|
|
3137
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
3113
3138
|
this.ws.send(JSON.stringify(msg));
|
|
3114
3139
|
}
|
|
3115
3140
|
}
|
|
@@ -3118,7 +3143,7 @@ var HyperliquidWebSocket = class {
|
|
|
3118
3143
|
// src/perp/recorder.ts
|
|
3119
3144
|
var import_viem4 = require("viem");
|
|
3120
3145
|
var import_chains2 = require("viem/chains");
|
|
3121
|
-
var
|
|
3146
|
+
var import_accounts2 = require("viem/accounts");
|
|
3122
3147
|
var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
|
|
3123
3148
|
var ROUTER_ABI = [
|
|
3124
3149
|
{
|
|
@@ -3157,7 +3182,7 @@ var PerpTradeRecorder = class {
|
|
|
3157
3182
|
constructor(opts) {
|
|
3158
3183
|
this.agentId = opts.agentId;
|
|
3159
3184
|
this.configHash = opts.configHash;
|
|
3160
|
-
this.account = (0,
|
|
3185
|
+
this.account = (0, import_accounts2.privateKeyToAccount)(opts.privateKey);
|
|
3161
3186
|
const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
|
|
3162
3187
|
const transport = (0, import_viem4.http)(rpcUrl);
|
|
3163
3188
|
this.publicClient = (0, import_viem4.createPublicClient)({
|
|
@@ -3298,199 +3323,650 @@ var PerpTradeRecorder = class {
|
|
|
3298
3323
|
// CONVERSION HELPERS
|
|
3299
3324
|
// ============================================================
|
|
3300
3325
|
/**
|
|
3301
|
-
* Calculate notional USD from a fill (6-decimal).
|
|
3302
|
-
* notionalUSD = px * sz * 1e6
|
|
3326
|
+
* Calculate notional USD from a fill (6-decimal).
|
|
3327
|
+
* notionalUSD = px * sz * 1e6
|
|
3328
|
+
*/
|
|
3329
|
+
calculateNotionalUSD(fill) {
|
|
3330
|
+
const px = parseFloat(fill.px);
|
|
3331
|
+
const sz = parseFloat(fill.sz);
|
|
3332
|
+
return BigInt(Math.round(px * sz * 1e6));
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Calculate fee USD from a fill (6-decimal).
|
|
3336
|
+
* feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
|
|
3337
|
+
*/
|
|
3338
|
+
calculateFeeUSD(fill) {
|
|
3339
|
+
const fee = parseFloat(fill.fee);
|
|
3340
|
+
const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
|
|
3341
|
+
return BigInt(Math.round((fee + builderFee) * 1e6));
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
|
|
3345
|
+
// src/perp/onboarding.ts
|
|
3346
|
+
var PerpOnboarding = class {
|
|
3347
|
+
client;
|
|
3348
|
+
signer;
|
|
3349
|
+
config;
|
|
3350
|
+
constructor(client, signer, config) {
|
|
3351
|
+
this.client = client;
|
|
3352
|
+
this.signer = signer;
|
|
3353
|
+
this.config = config;
|
|
3354
|
+
}
|
|
3355
|
+
// ============================================================
|
|
3356
|
+
// BUILDER FEE
|
|
3357
|
+
// ============================================================
|
|
3358
|
+
/**
|
|
3359
|
+
* Check if the user has approved the builder fee.
|
|
3360
|
+
* Builder fee must be approved before orders can include builder fees.
|
|
3361
|
+
*/
|
|
3362
|
+
async isBuilderFeeApproved() {
|
|
3363
|
+
try {
|
|
3364
|
+
const maxFee = await this.client.getMaxBuilderFee(
|
|
3365
|
+
this.signer.getAddress(),
|
|
3366
|
+
this.config.builderAddress
|
|
3367
|
+
);
|
|
3368
|
+
return maxFee >= this.config.builderFeeTenthsBps;
|
|
3369
|
+
} catch {
|
|
3370
|
+
return false;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
/**
|
|
3374
|
+
* Approve the builder fee on Hyperliquid.
|
|
3375
|
+
* This is a one-time approval per builder address.
|
|
3376
|
+
*/
|
|
3377
|
+
async approveBuilderFee() {
|
|
3378
|
+
try {
|
|
3379
|
+
const action = {
|
|
3380
|
+
type: "approveBuilderFee",
|
|
3381
|
+
hyperliquidChain: "Mainnet",
|
|
3382
|
+
maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
|
|
3383
|
+
builder: this.config.builderAddress,
|
|
3384
|
+
nonce: Number(getNextNonce())
|
|
3385
|
+
};
|
|
3386
|
+
const { signature } = await this.signer.signApproval(action);
|
|
3387
|
+
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
3388
|
+
method: "POST",
|
|
3389
|
+
headers: { "Content-Type": "application/json" },
|
|
3390
|
+
body: JSON.stringify({
|
|
3391
|
+
action,
|
|
3392
|
+
signature: {
|
|
3393
|
+
r: signature.slice(0, 66),
|
|
3394
|
+
s: `0x${signature.slice(66, 130)}`,
|
|
3395
|
+
v: parseInt(signature.slice(130, 132), 16)
|
|
3396
|
+
},
|
|
3397
|
+
nonce: action.nonce,
|
|
3398
|
+
vaultAddress: null
|
|
3399
|
+
})
|
|
3400
|
+
});
|
|
3401
|
+
if (!resp.ok) {
|
|
3402
|
+
const text = await resp.text();
|
|
3403
|
+
console.error(`Builder fee approval failed: ${resp.status} ${text}`);
|
|
3404
|
+
return false;
|
|
3405
|
+
}
|
|
3406
|
+
console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
|
|
3407
|
+
return true;
|
|
3408
|
+
} catch (error) {
|
|
3409
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3410
|
+
console.error(`Builder fee approval failed: ${message}`);
|
|
3411
|
+
return false;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
// ============================================================
|
|
3415
|
+
// BALANCE & REQUIREMENTS
|
|
3416
|
+
// ============================================================
|
|
3417
|
+
/**
|
|
3418
|
+
* Check if the user has sufficient USDC balance on Hyperliquid.
|
|
3419
|
+
* Returns the account equity in USD.
|
|
3420
|
+
*/
|
|
3421
|
+
async checkBalance() {
|
|
3422
|
+
try {
|
|
3423
|
+
const account = await this.client.getAccountSummary(this.signer.getAddress());
|
|
3424
|
+
return {
|
|
3425
|
+
hasBalance: account.totalEquity > 0,
|
|
3426
|
+
equity: account.totalEquity
|
|
3427
|
+
};
|
|
3428
|
+
} catch {
|
|
3429
|
+
return { hasBalance: false, equity: 0 };
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Verify that the agent's risk universe allows perp trading.
|
|
3434
|
+
* Perps require risk universe >= 2 (Derivatives or higher).
|
|
3435
|
+
*/
|
|
3436
|
+
verifyRiskUniverse(riskUniverse) {
|
|
3437
|
+
if (riskUniverse >= 2) {
|
|
3438
|
+
return {
|
|
3439
|
+
allowed: true,
|
|
3440
|
+
message: `Risk universe ${riskUniverse} allows perp trading`
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
return {
|
|
3444
|
+
allowed: false,
|
|
3445
|
+
message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
// ============================================================
|
|
3449
|
+
// FULL ONBOARDING CHECK
|
|
3450
|
+
// ============================================================
|
|
3451
|
+
/**
|
|
3452
|
+
* Run all onboarding checks and return status.
|
|
3453
|
+
* Does NOT auto-approve — caller must explicitly approve after review.
|
|
3454
|
+
*/
|
|
3455
|
+
async checkOnboardingStatus(riskUniverse) {
|
|
3456
|
+
const riskCheck = this.verifyRiskUniverse(riskUniverse);
|
|
3457
|
+
const balanceCheck = await this.checkBalance();
|
|
3458
|
+
const builderFeeApproved = await this.isBuilderFeeApproved();
|
|
3459
|
+
const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
|
|
3460
|
+
return {
|
|
3461
|
+
ready,
|
|
3462
|
+
riskUniverseOk: riskCheck.allowed,
|
|
3463
|
+
riskUniverseMessage: riskCheck.message,
|
|
3464
|
+
hasBalance: balanceCheck.hasBalance,
|
|
3465
|
+
equity: balanceCheck.equity,
|
|
3466
|
+
builderFeeApproved,
|
|
3467
|
+
builderAddress: this.config.builderAddress,
|
|
3468
|
+
builderFeeBps: this.config.builderFeeTenthsBps / 10
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
/**
|
|
3472
|
+
* Run full onboarding: check status and auto-approve builder fee if needed.
|
|
3473
|
+
* Returns the final status after all actions.
|
|
3474
|
+
*/
|
|
3475
|
+
async onboard(riskUniverse) {
|
|
3476
|
+
let status = await this.checkOnboardingStatus(riskUniverse);
|
|
3477
|
+
if (!status.riskUniverseOk) {
|
|
3478
|
+
console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
|
|
3479
|
+
return status;
|
|
3480
|
+
}
|
|
3481
|
+
if (!status.hasBalance) {
|
|
3482
|
+
console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
|
|
3483
|
+
return status;
|
|
3484
|
+
}
|
|
3485
|
+
if (!status.builderFeeApproved) {
|
|
3486
|
+
console.log("Approving builder fee...");
|
|
3487
|
+
const approved = await this.approveBuilderFee();
|
|
3488
|
+
if (approved) {
|
|
3489
|
+
status = { ...status, builderFeeApproved: true, ready: true };
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (status.ready) {
|
|
3493
|
+
console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
|
|
3494
|
+
}
|
|
3495
|
+
return status;
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
|
|
3499
|
+
// src/perp/funding.ts
|
|
3500
|
+
var import_viem5 = require("viem");
|
|
3501
|
+
var import_chains3 = require("viem/chains");
|
|
3502
|
+
var import_accounts3 = require("viem/accounts");
|
|
3503
|
+
var ERC20_ABI = (0, import_viem5.parseAbi)([
|
|
3504
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
3505
|
+
"function balanceOf(address account) external view returns (uint256)",
|
|
3506
|
+
"function allowance(address owner, address spender) external view returns (uint256)"
|
|
3507
|
+
]);
|
|
3508
|
+
var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
|
|
3509
|
+
"function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
|
|
3510
|
+
"event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
|
|
3511
|
+
]);
|
|
3512
|
+
var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
|
|
3513
|
+
"function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
|
|
3514
|
+
"event MessageSent(bytes message)"
|
|
3515
|
+
]);
|
|
3516
|
+
var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
|
|
3517
|
+
"function deposit(uint256 amount, uint32 destinationDex) external"
|
|
3518
|
+
]);
|
|
3519
|
+
|
|
3520
|
+
// src/secure-env.ts
|
|
3521
|
+
var crypto = __toESM(require("crypto"));
|
|
3522
|
+
var fs = __toESM(require("fs"));
|
|
3523
|
+
var path = __toESM(require("path"));
|
|
3524
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3525
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
3526
|
+
var SALT_LENGTH = 32;
|
|
3527
|
+
var IV_LENGTH = 16;
|
|
3528
|
+
var KEY_LENGTH = 32;
|
|
3529
|
+
var SENSITIVE_PATTERNS = [
|
|
3530
|
+
/PRIVATE_KEY$/i,
|
|
3531
|
+
/_API_KEY$/i,
|
|
3532
|
+
/API_KEY$/i,
|
|
3533
|
+
/_SECRET$/i,
|
|
3534
|
+
/^OPENAI_API_KEY$/i,
|
|
3535
|
+
/^ANTHROPIC_API_KEY$/i,
|
|
3536
|
+
/^GOOGLE_AI_API_KEY$/i,
|
|
3537
|
+
/^DEEPSEEK_API_KEY$/i,
|
|
3538
|
+
/^MISTRAL_API_KEY$/i,
|
|
3539
|
+
/^GROQ_API_KEY$/i,
|
|
3540
|
+
/^TOGETHER_API_KEY$/i
|
|
3541
|
+
];
|
|
3542
|
+
function isSensitiveKey(key) {
|
|
3543
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
3544
|
+
}
|
|
3545
|
+
function deriveKey(passphrase, salt) {
|
|
3546
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
3547
|
+
}
|
|
3548
|
+
function encryptValue(value, key) {
|
|
3549
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
3550
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
3551
|
+
let encrypted = cipher.update(value, "utf8", "hex");
|
|
3552
|
+
encrypted += cipher.final("hex");
|
|
3553
|
+
const tag = cipher.getAuthTag();
|
|
3554
|
+
return {
|
|
3555
|
+
iv: iv.toString("hex"),
|
|
3556
|
+
encrypted,
|
|
3557
|
+
tag: tag.toString("hex")
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
function decryptValue(encrypted, key, iv, tag) {
|
|
3561
|
+
const decipher = crypto.createDecipheriv(
|
|
3562
|
+
ALGORITHM,
|
|
3563
|
+
key,
|
|
3564
|
+
Buffer.from(iv, "hex")
|
|
3565
|
+
);
|
|
3566
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
3567
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
3568
|
+
decrypted += decipher.final("utf8");
|
|
3569
|
+
return decrypted;
|
|
3570
|
+
}
|
|
3571
|
+
function parseEnvFile(content) {
|
|
3572
|
+
const entries = [];
|
|
3573
|
+
for (const line of content.split("\n")) {
|
|
3574
|
+
const trimmed = line.trim();
|
|
3575
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
3576
|
+
const eqIndex = trimmed.indexOf("=");
|
|
3577
|
+
if (eqIndex === -1) continue;
|
|
3578
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
3579
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
3580
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3581
|
+
value = value.slice(1, -1);
|
|
3582
|
+
}
|
|
3583
|
+
if (key && value) {
|
|
3584
|
+
entries.push({ key, value });
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
return entries;
|
|
3588
|
+
}
|
|
3589
|
+
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
3590
|
+
if (!fs.existsSync(envPath)) {
|
|
3591
|
+
throw new Error(`File not found: ${envPath}`);
|
|
3592
|
+
}
|
|
3593
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
3594
|
+
const entries = parseEnvFile(content);
|
|
3595
|
+
if (entries.length === 0) {
|
|
3596
|
+
throw new Error("No environment variables found in file");
|
|
3597
|
+
}
|
|
3598
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
3599
|
+
const key = deriveKey(passphrase, salt);
|
|
3600
|
+
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
3601
|
+
if (isSensitiveKey(envKey)) {
|
|
3602
|
+
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
3603
|
+
return {
|
|
3604
|
+
key: envKey,
|
|
3605
|
+
value: encrypted,
|
|
3606
|
+
encrypted: true,
|
|
3607
|
+
iv,
|
|
3608
|
+
tag
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
return {
|
|
3612
|
+
key: envKey,
|
|
3613
|
+
value,
|
|
3614
|
+
encrypted: false
|
|
3615
|
+
};
|
|
3616
|
+
});
|
|
3617
|
+
const encryptedEnv = {
|
|
3618
|
+
version: 1,
|
|
3619
|
+
salt: salt.toString("hex"),
|
|
3620
|
+
entries: encryptedEntries
|
|
3621
|
+
};
|
|
3622
|
+
const encPath = envPath + ".enc";
|
|
3623
|
+
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
3624
|
+
if (deleteOriginal) {
|
|
3625
|
+
fs.unlinkSync(envPath);
|
|
3626
|
+
}
|
|
3627
|
+
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
3628
|
+
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
3629
|
+
console.log(
|
|
3630
|
+
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
3631
|
+
);
|
|
3632
|
+
return encPath;
|
|
3633
|
+
}
|
|
3634
|
+
function decryptEnvFile(encPath, passphrase) {
|
|
3635
|
+
if (!fs.existsSync(encPath)) {
|
|
3636
|
+
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
3637
|
+
}
|
|
3638
|
+
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
3639
|
+
if (content.version !== 1) {
|
|
3640
|
+
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
3641
|
+
}
|
|
3642
|
+
const salt = Buffer.from(content.salt, "hex");
|
|
3643
|
+
const key = deriveKey(passphrase, salt);
|
|
3644
|
+
const result = {};
|
|
3645
|
+
for (const entry of content.entries) {
|
|
3646
|
+
if (entry.encrypted) {
|
|
3647
|
+
if (!entry.iv || !entry.tag) {
|
|
3648
|
+
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
3649
|
+
}
|
|
3650
|
+
try {
|
|
3651
|
+
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
3652
|
+
} catch {
|
|
3653
|
+
throw new Error(
|
|
3654
|
+
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
} else {
|
|
3658
|
+
result[entry.key] = entry.value;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
return result;
|
|
3662
|
+
}
|
|
3663
|
+
function loadSecureEnv(basePath, passphrase) {
|
|
3664
|
+
const encPath = path.join(basePath, ".env.enc");
|
|
3665
|
+
const envPath = path.join(basePath, ".env");
|
|
3666
|
+
if (fs.existsSync(encPath)) {
|
|
3667
|
+
if (!passphrase) {
|
|
3668
|
+
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
3669
|
+
}
|
|
3670
|
+
if (!passphrase) {
|
|
3671
|
+
console.warn("");
|
|
3672
|
+
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
3673
|
+
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
3674
|
+
console.warn(" pass --passphrase when running the agent.");
|
|
3675
|
+
console.warn(" Falling back to plaintext .env file.");
|
|
3676
|
+
console.warn("");
|
|
3677
|
+
} else {
|
|
3678
|
+
const vars = decryptEnvFile(encPath, passphrase);
|
|
3679
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
3680
|
+
process.env[key] = value;
|
|
3681
|
+
}
|
|
3682
|
+
return true;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
if (fs.existsSync(envPath)) {
|
|
3686
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
3687
|
+
const entries = parseEnvFile(content);
|
|
3688
|
+
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
3689
|
+
if (sensitiveKeys.length > 0) {
|
|
3690
|
+
console.warn("");
|
|
3691
|
+
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
3692
|
+
for (const key of sensitiveKeys) {
|
|
3693
|
+
console.warn(` - ${key}`);
|
|
3694
|
+
}
|
|
3695
|
+
console.warn("");
|
|
3696
|
+
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
3697
|
+
console.warn("");
|
|
3698
|
+
}
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
return false;
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// src/index.ts
|
|
3705
|
+
var AGENT_VERSION = "0.1.22";
|
|
3706
|
+
|
|
3707
|
+
// src/relay.ts
|
|
3708
|
+
var RelayClient = class {
|
|
3709
|
+
config;
|
|
3710
|
+
ws = null;
|
|
3711
|
+
authenticated = false;
|
|
3712
|
+
authRejected = false;
|
|
3713
|
+
reconnectAttempts = 0;
|
|
3714
|
+
maxReconnectAttempts = 50;
|
|
3715
|
+
reconnectTimer = null;
|
|
3716
|
+
heartbeatTimer = null;
|
|
3717
|
+
stopped = false;
|
|
3718
|
+
constructor(config) {
|
|
3719
|
+
this.config = config;
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* Connect to the relay server
|
|
3723
|
+
*/
|
|
3724
|
+
async connect() {
|
|
3725
|
+
if (this.stopped) return;
|
|
3726
|
+
const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
|
|
3727
|
+
return new Promise((resolve, reject) => {
|
|
3728
|
+
try {
|
|
3729
|
+
this.ws = new import_ws2.default(wsUrl);
|
|
3730
|
+
} catch (error) {
|
|
3731
|
+
console.error("Relay: Failed to create WebSocket:", error);
|
|
3732
|
+
this.scheduleReconnect();
|
|
3733
|
+
reject(error);
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
const connectTimeout = setTimeout(() => {
|
|
3737
|
+
if (!this.authenticated) {
|
|
3738
|
+
console.error("Relay: Connection timeout");
|
|
3739
|
+
this.ws?.close();
|
|
3740
|
+
this.scheduleReconnect();
|
|
3741
|
+
reject(new Error("Connection timeout"));
|
|
3742
|
+
}
|
|
3743
|
+
}, 15e3);
|
|
3744
|
+
this.ws.on("open", async () => {
|
|
3745
|
+
this.authRejected = false;
|
|
3746
|
+
console.log("Relay: Connected, authenticating...");
|
|
3747
|
+
try {
|
|
3748
|
+
await this.authenticate();
|
|
3749
|
+
} catch (error) {
|
|
3750
|
+
console.error("Relay: Authentication failed:", error);
|
|
3751
|
+
this.ws?.close();
|
|
3752
|
+
clearTimeout(connectTimeout);
|
|
3753
|
+
reject(error);
|
|
3754
|
+
}
|
|
3755
|
+
});
|
|
3756
|
+
this.ws.on("message", (raw) => {
|
|
3757
|
+
try {
|
|
3758
|
+
const data = JSON.parse(raw.toString());
|
|
3759
|
+
this.handleMessage(data);
|
|
3760
|
+
if (data.type === "auth_success") {
|
|
3761
|
+
clearTimeout(connectTimeout);
|
|
3762
|
+
this.authenticated = true;
|
|
3763
|
+
this.reconnectAttempts = 0;
|
|
3764
|
+
this.startHeartbeat();
|
|
3765
|
+
console.log("Relay: Authenticated successfully");
|
|
3766
|
+
resolve();
|
|
3767
|
+
} else if (data.type === "auth_error") {
|
|
3768
|
+
clearTimeout(connectTimeout);
|
|
3769
|
+
this.authRejected = true;
|
|
3770
|
+
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
3771
|
+
reject(new Error(data.message));
|
|
3772
|
+
}
|
|
3773
|
+
} catch {
|
|
3774
|
+
}
|
|
3775
|
+
});
|
|
3776
|
+
this.ws.on("close", (code, reason) => {
|
|
3777
|
+
clearTimeout(connectTimeout);
|
|
3778
|
+
this.authenticated = false;
|
|
3779
|
+
this.stopHeartbeat();
|
|
3780
|
+
if (!this.stopped) {
|
|
3781
|
+
if (!this.authRejected) {
|
|
3782
|
+
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
3783
|
+
}
|
|
3784
|
+
this.scheduleReconnect();
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
this.ws.on("error", (error) => {
|
|
3788
|
+
if (!this.stopped) {
|
|
3789
|
+
console.error("Relay: WebSocket error:", error.message);
|
|
3790
|
+
}
|
|
3791
|
+
});
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
/**
|
|
3795
|
+
* Authenticate with the relay server using wallet signature
|
|
3796
|
+
*/
|
|
3797
|
+
async authenticate() {
|
|
3798
|
+
const account = (0, import_accounts4.privateKeyToAccount)(this.config.privateKey);
|
|
3799
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
3800
|
+
const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
|
|
3801
|
+
const signature = await (0, import_accounts4.signMessage)({
|
|
3802
|
+
message,
|
|
3803
|
+
privateKey: this.config.privateKey
|
|
3804
|
+
});
|
|
3805
|
+
this.send({
|
|
3806
|
+
type: "auth",
|
|
3807
|
+
agentId: this.config.agentId,
|
|
3808
|
+
wallet: account.address,
|
|
3809
|
+
timestamp,
|
|
3810
|
+
signature,
|
|
3811
|
+
sdkVersion: AGENT_VERSION
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Handle incoming messages from the relay server
|
|
3816
|
+
*/
|
|
3817
|
+
handleMessage(data) {
|
|
3818
|
+
switch (data.type) {
|
|
3819
|
+
case "command":
|
|
3820
|
+
if (data.command && this.config.onCommand) {
|
|
3821
|
+
this.config.onCommand(data.command);
|
|
3822
|
+
}
|
|
3823
|
+
break;
|
|
3824
|
+
case "auth_success":
|
|
3825
|
+
case "auth_error":
|
|
3826
|
+
break;
|
|
3827
|
+
case "error":
|
|
3828
|
+
console.error(`Relay: Server error: ${data.message}`);
|
|
3829
|
+
break;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* Send a status heartbeat
|
|
3303
3834
|
*/
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3835
|
+
sendHeartbeat(status) {
|
|
3836
|
+
if (!this.authenticated) return;
|
|
3837
|
+
this.send({
|
|
3838
|
+
type: "heartbeat",
|
|
3839
|
+
agentId: this.config.agentId,
|
|
3840
|
+
status
|
|
3841
|
+
});
|
|
3308
3842
|
}
|
|
3309
3843
|
/**
|
|
3310
|
-
*
|
|
3311
|
-
* feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
|
|
3844
|
+
* Send a status update (outside of regular heartbeat)
|
|
3312
3845
|
*/
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3846
|
+
sendStatusUpdate(status) {
|
|
3847
|
+
if (!this.authenticated) return;
|
|
3848
|
+
this.send({
|
|
3849
|
+
type: "status_update",
|
|
3850
|
+
agentId: this.config.agentId,
|
|
3851
|
+
status
|
|
3852
|
+
});
|
|
3317
3853
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3854
|
+
/**
|
|
3855
|
+
* Send a message to the command center
|
|
3856
|
+
*/
|
|
3857
|
+
sendMessage(messageType, level, title, body, data) {
|
|
3858
|
+
if (!this.authenticated) return;
|
|
3859
|
+
this.send({
|
|
3860
|
+
type: "message",
|
|
3861
|
+
agentId: this.config.agentId,
|
|
3862
|
+
messageType,
|
|
3863
|
+
level,
|
|
3864
|
+
title,
|
|
3865
|
+
body,
|
|
3866
|
+
data
|
|
3867
|
+
});
|
|
3329
3868
|
}
|
|
3330
|
-
// ============================================================
|
|
3331
|
-
// BUILDER FEE
|
|
3332
|
-
// ============================================================
|
|
3333
3869
|
/**
|
|
3334
|
-
*
|
|
3335
|
-
* Builder fee must be approved before orders can include builder fees.
|
|
3870
|
+
* Send a command execution result
|
|
3336
3871
|
*/
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
}
|
|
3872
|
+
sendCommandResult(commandId, success, result) {
|
|
3873
|
+
if (!this.authenticated) return;
|
|
3874
|
+
this.send({
|
|
3875
|
+
type: "command_result",
|
|
3876
|
+
agentId: this.config.agentId,
|
|
3877
|
+
commandId,
|
|
3878
|
+
success,
|
|
3879
|
+
result
|
|
3880
|
+
});
|
|
3347
3881
|
}
|
|
3348
3882
|
/**
|
|
3349
|
-
*
|
|
3350
|
-
* This is a one-time approval per builder address.
|
|
3883
|
+
* Start the heartbeat timer
|
|
3351
3884
|
*/
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
builder: this.config.builderAddress,
|
|
3359
|
-
nonce: Number(getNextNonce())
|
|
3360
|
-
};
|
|
3361
|
-
const { signature } = await this.signer.signApproval(action);
|
|
3362
|
-
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
3363
|
-
method: "POST",
|
|
3364
|
-
headers: { "Content-Type": "application/json" },
|
|
3365
|
-
body: JSON.stringify({
|
|
3366
|
-
action,
|
|
3367
|
-
signature: {
|
|
3368
|
-
r: signature.slice(0, 66),
|
|
3369
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
3370
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
3371
|
-
},
|
|
3372
|
-
nonce: action.nonce,
|
|
3373
|
-
vaultAddress: null
|
|
3374
|
-
})
|
|
3375
|
-
});
|
|
3376
|
-
if (!resp.ok) {
|
|
3377
|
-
const text = await resp.text();
|
|
3378
|
-
console.error(`Builder fee approval failed: ${resp.status} ${text}`);
|
|
3379
|
-
return false;
|
|
3885
|
+
startHeartbeat() {
|
|
3886
|
+
this.stopHeartbeat();
|
|
3887
|
+
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
3888
|
+
this.heartbeatTimer = setInterval(() => {
|
|
3889
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
3890
|
+
this.ws.ping();
|
|
3380
3891
|
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3892
|
+
}, interval);
|
|
3893
|
+
}
|
|
3894
|
+
/**
|
|
3895
|
+
* Stop the heartbeat timer
|
|
3896
|
+
*/
|
|
3897
|
+
stopHeartbeat() {
|
|
3898
|
+
if (this.heartbeatTimer) {
|
|
3899
|
+
clearInterval(this.heartbeatTimer);
|
|
3900
|
+
this.heartbeatTimer = null;
|
|
3387
3901
|
}
|
|
3388
3902
|
}
|
|
3389
|
-
// ============================================================
|
|
3390
|
-
// BALANCE & REQUIREMENTS
|
|
3391
|
-
// ============================================================
|
|
3392
3903
|
/**
|
|
3393
|
-
*
|
|
3394
|
-
* Returns the account equity in USD.
|
|
3904
|
+
* Schedule a reconnection with exponential backoff
|
|
3395
3905
|
*/
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
equity: account.totalEquity
|
|
3402
|
-
};
|
|
3403
|
-
} catch {
|
|
3404
|
-
return { hasBalance: false, equity: 0 };
|
|
3906
|
+
scheduleReconnect() {
|
|
3907
|
+
if (this.stopped) return;
|
|
3908
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
3909
|
+
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
3910
|
+
return;
|
|
3405
3911
|
}
|
|
3912
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
3913
|
+
this.reconnectAttempts++;
|
|
3914
|
+
console.log(
|
|
3915
|
+
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
3916
|
+
);
|
|
3917
|
+
this.reconnectTimer = setTimeout(() => {
|
|
3918
|
+
this.connect().catch(() => {
|
|
3919
|
+
});
|
|
3920
|
+
}, delay);
|
|
3406
3921
|
}
|
|
3407
3922
|
/**
|
|
3408
|
-
*
|
|
3409
|
-
* Perps require risk universe >= 2 (Derivatives or higher).
|
|
3923
|
+
* Send a JSON message to the WebSocket
|
|
3410
3924
|
*/
|
|
3411
|
-
|
|
3412
|
-
if (
|
|
3413
|
-
|
|
3414
|
-
allowed: true,
|
|
3415
|
-
message: `Risk universe ${riskUniverse} allows perp trading`
|
|
3416
|
-
};
|
|
3925
|
+
send(data) {
|
|
3926
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
3927
|
+
this.ws.send(JSON.stringify(data));
|
|
3417
3928
|
}
|
|
3418
|
-
return {
|
|
3419
|
-
allowed: false,
|
|
3420
|
-
message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
|
|
3421
|
-
};
|
|
3422
3929
|
}
|
|
3423
|
-
// ============================================================
|
|
3424
|
-
// FULL ONBOARDING CHECK
|
|
3425
|
-
// ============================================================
|
|
3426
3930
|
/**
|
|
3427
|
-
*
|
|
3428
|
-
* Does NOT auto-approve — caller must explicitly approve after review.
|
|
3931
|
+
* Check if connected and authenticated
|
|
3429
3932
|
*/
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
const balanceCheck = await this.checkBalance();
|
|
3433
|
-
const builderFeeApproved = await this.isBuilderFeeApproved();
|
|
3434
|
-
const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
|
|
3435
|
-
return {
|
|
3436
|
-
ready,
|
|
3437
|
-
riskUniverseOk: riskCheck.allowed,
|
|
3438
|
-
riskUniverseMessage: riskCheck.message,
|
|
3439
|
-
hasBalance: balanceCheck.hasBalance,
|
|
3440
|
-
equity: balanceCheck.equity,
|
|
3441
|
-
builderFeeApproved,
|
|
3442
|
-
builderAddress: this.config.builderAddress,
|
|
3443
|
-
builderFeeBps: this.config.builderFeeTenthsBps / 10
|
|
3444
|
-
};
|
|
3933
|
+
get isConnected() {
|
|
3934
|
+
return this.authenticated && this.ws?.readyState === import_ws2.default.OPEN;
|
|
3445
3935
|
}
|
|
3446
3936
|
/**
|
|
3447
|
-
*
|
|
3448
|
-
* Returns the final status after all actions.
|
|
3937
|
+
* Disconnect and stop reconnecting
|
|
3449
3938
|
*/
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
if (!status.hasBalance) {
|
|
3457
|
-
console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
|
|
3458
|
-
return status;
|
|
3459
|
-
}
|
|
3460
|
-
if (!status.builderFeeApproved) {
|
|
3461
|
-
console.log("Approving builder fee...");
|
|
3462
|
-
const approved = await this.approveBuilderFee();
|
|
3463
|
-
if (approved) {
|
|
3464
|
-
status = { ...status, builderFeeApproved: true, ready: true };
|
|
3465
|
-
}
|
|
3939
|
+
disconnect() {
|
|
3940
|
+
this.stopped = true;
|
|
3941
|
+
this.stopHeartbeat();
|
|
3942
|
+
if (this.reconnectTimer) {
|
|
3943
|
+
clearTimeout(this.reconnectTimer);
|
|
3944
|
+
this.reconnectTimer = null;
|
|
3466
3945
|
}
|
|
3467
|
-
if (
|
|
3468
|
-
|
|
3946
|
+
if (this.ws) {
|
|
3947
|
+
this.ws.close(1e3, "Agent shutting down");
|
|
3948
|
+
this.ws = null;
|
|
3469
3949
|
}
|
|
3470
|
-
|
|
3950
|
+
this.authenticated = false;
|
|
3951
|
+
console.log("Relay: Disconnected");
|
|
3471
3952
|
}
|
|
3472
3953
|
};
|
|
3473
3954
|
|
|
3474
|
-
// src/
|
|
3475
|
-
var
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
"event MessageSent(bytes message)"
|
|
3490
|
-
]);
|
|
3491
|
-
var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
|
|
3492
|
-
"function deposit(uint256 amount, uint32 destinationDex) external"
|
|
3493
|
-
]);
|
|
3955
|
+
// src/browser-open.ts
|
|
3956
|
+
var import_child_process2 = require("child_process");
|
|
3957
|
+
function openBrowser(url) {
|
|
3958
|
+
const platform = process.platform;
|
|
3959
|
+
try {
|
|
3960
|
+
if (platform === "darwin") {
|
|
3961
|
+
(0, import_child_process2.exec)(`open "${url}"`);
|
|
3962
|
+
} else if (platform === "win32") {
|
|
3963
|
+
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
3964
|
+
} else {
|
|
3965
|
+
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
3966
|
+
}
|
|
3967
|
+
} catch {
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3494
3970
|
|
|
3495
3971
|
// src/runtime.ts
|
|
3496
3972
|
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
@@ -3508,14 +3984,19 @@ var AgentRuntime = class {
|
|
|
3508
3984
|
isRunning = false;
|
|
3509
3985
|
mode = "idle";
|
|
3510
3986
|
configHash;
|
|
3987
|
+
pendingConfigHash = null;
|
|
3988
|
+
lastConfigCheckAt = 0;
|
|
3989
|
+
// Timestamp of last pending config RPC check
|
|
3511
3990
|
cycleCount = 0;
|
|
3512
3991
|
lastCycleAt = 0;
|
|
3513
3992
|
lastPortfolioValue = 0;
|
|
3514
3993
|
lastEthBalance = "0";
|
|
3994
|
+
lastPrices = {};
|
|
3515
3995
|
processAlive = true;
|
|
3516
3996
|
riskUniverse = 0;
|
|
3517
3997
|
allowedTokens = /* @__PURE__ */ new Set();
|
|
3518
3998
|
strategyContext;
|
|
3999
|
+
positionTracker;
|
|
3519
4000
|
// Perp trading components (null if perp not enabled)
|
|
3520
4001
|
perpClient = null;
|
|
3521
4002
|
perpSigner = null;
|
|
@@ -3532,6 +4013,12 @@ var AgentRuntime = class {
|
|
|
3532
4013
|
// When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
|
|
3533
4014
|
perpConnected = false;
|
|
3534
4015
|
perpTradingActive = false;
|
|
4016
|
+
// Cached perp account data for synchronous heartbeat inclusion (refreshed async)
|
|
4017
|
+
cachedPerpEquity = 0;
|
|
4018
|
+
cachedPerpUnrealizedPnl = 0;
|
|
4019
|
+
cachedPerpMarginUsed = 0;
|
|
4020
|
+
cachedPerpLeverage = 0;
|
|
4021
|
+
cachedPerpOpenPositions = 0;
|
|
3535
4022
|
constructor(config) {
|
|
3536
4023
|
this.config = config;
|
|
3537
4024
|
}
|
|
@@ -3540,7 +4027,7 @@ var AgentRuntime = class {
|
|
|
3540
4027
|
*/
|
|
3541
4028
|
async initialize() {
|
|
3542
4029
|
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
3543
|
-
this.client = new
|
|
4030
|
+
this.client = new import_sdk.ExagentClient({
|
|
3544
4031
|
privateKey: this.config.privateKey,
|
|
3545
4032
|
network: this.config.network
|
|
3546
4033
|
});
|
|
@@ -3558,14 +4045,20 @@ var AgentRuntime = class {
|
|
|
3558
4045
|
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
3559
4046
|
await this.syncConfigHash();
|
|
3560
4047
|
this.strategy = await loadStrategy();
|
|
4048
|
+
const store = new FileStore();
|
|
3561
4049
|
this.strategyContext = {
|
|
3562
|
-
store
|
|
4050
|
+
store,
|
|
3563
4051
|
agentId: Number(this.config.agentId),
|
|
3564
4052
|
walletAddress: this.client.address
|
|
3565
4053
|
};
|
|
4054
|
+
this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
|
|
3566
4055
|
this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
|
|
3567
4056
|
this.riskManager = new RiskManager(this.config.trading);
|
|
3568
4057
|
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
4058
|
+
const savedRisk = this.positionTracker.getRiskState();
|
|
4059
|
+
if (savedRisk.lastResetDate) {
|
|
4060
|
+
this.riskManager.restoreState(savedRisk);
|
|
4061
|
+
}
|
|
3569
4062
|
await this.initializeVaultManager();
|
|
3570
4063
|
await this.initializePerp();
|
|
3571
4064
|
await this.initializeRelay();
|
|
@@ -3751,7 +4244,7 @@ var AgentRuntime = class {
|
|
|
3751
4244
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
3752
4245
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
3753
4246
|
const nonce = await this.client.registry.getNonce(address);
|
|
3754
|
-
const linkMessage =
|
|
4247
|
+
const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
|
|
3755
4248
|
address,
|
|
3756
4249
|
agentId,
|
|
3757
4250
|
nonce
|
|
@@ -3836,65 +4329,104 @@ var AgentRuntime = class {
|
|
|
3836
4329
|
}
|
|
3837
4330
|
/**
|
|
3838
4331
|
* Sync the LLM config hash to chain for epoch tracking.
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
4332
|
+
*
|
|
4333
|
+
* If the trading wallet is NOT the agent owner, the on-chain setConfig
|
|
4334
|
+
* call would revert with AgentNotOwner. Instead, we set a pending state
|
|
4335
|
+
* and send a message to the command center so the owner can approve it
|
|
4336
|
+
* from the website with one click.
|
|
4337
|
+
*
|
|
4338
|
+
* Until the config is verified on-chain, the agent won't appear on the
|
|
4339
|
+
* leaderboard (trades still execute normally).
|
|
3841
4340
|
*/
|
|
3842
4341
|
async syncConfigHash() {
|
|
3843
4342
|
const agentId = BigInt(this.config.agentId);
|
|
3844
4343
|
const llmMeta = this.llm.getMetadata();
|
|
3845
|
-
this.configHash =
|
|
4344
|
+
this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
3846
4345
|
console.log(`Config hash: ${this.configHash}`);
|
|
3847
4346
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
3848
|
-
if (onChainHash
|
|
3849
|
-
console.log("Config
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
4347
|
+
if (onChainHash === this.configHash) {
|
|
4348
|
+
console.log("Config hash matches on-chain");
|
|
4349
|
+
this.pendingConfigHash = null;
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
console.log("Config changed, updating on-chain...");
|
|
4353
|
+
const agent = await this.client.registry.getAgent(agentId);
|
|
4354
|
+
const isOwner = agent?.owner.toLowerCase() === this.client.address.toLowerCase();
|
|
4355
|
+
if (!isOwner) {
|
|
4356
|
+
this.pendingConfigHash = this.configHash;
|
|
4357
|
+
this.configHash = onChainHash;
|
|
4358
|
+
console.log("");
|
|
4359
|
+
console.log("=== CONFIG VERIFICATION NEEDED ===");
|
|
4360
|
+
console.log("");
|
|
4361
|
+
console.log(" Your trading wallet cannot update the LLM config on-chain.");
|
|
4362
|
+
console.log(" The owner must approve this from the command center.");
|
|
4363
|
+
console.log("");
|
|
4364
|
+
console.log(` LLM: ${llmMeta.provider} / ${llmMeta.model}`);
|
|
4365
|
+
console.log(` Hash: ${this.pendingConfigHash}`);
|
|
4366
|
+
console.log("");
|
|
4367
|
+
console.log(" Until approved:");
|
|
4368
|
+
console.log(" - New agents will not appear on the leaderboard");
|
|
4369
|
+
console.log(" - Trades will still execute normally");
|
|
4370
|
+
console.log("");
|
|
4371
|
+
this.relay?.sendMessage(
|
|
4372
|
+
"system",
|
|
4373
|
+
"warning",
|
|
4374
|
+
"Config Verification Needed",
|
|
4375
|
+
`Your agent is using ${llmMeta.provider}/${llmMeta.model} but this hasn't been recorded on-chain. Open the command center and click "Approve Config" to verify. Until then, your agent won't appear on the leaderboard.`,
|
|
4376
|
+
{ configHash: this.pendingConfigHash, provider: llmMeta.provider, model: llmMeta.model }
|
|
4377
|
+
);
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
try {
|
|
4381
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
4382
|
+
console.log(`Config updated on-chain`);
|
|
4383
|
+
this.pendingConfigHash = null;
|
|
4384
|
+
} catch (error) {
|
|
4385
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4386
|
+
if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
4387
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
4388
|
+
const publicClientInstance = (0, import_viem6.createPublicClient)({
|
|
4389
|
+
chain: import_chains4.base,
|
|
4390
|
+
transport: (0, import_viem6.http)(this.getRpcUrl())
|
|
4391
|
+
});
|
|
4392
|
+
console.log("");
|
|
4393
|
+
console.log("=== ETH NEEDED FOR GAS ===");
|
|
4394
|
+
console.log("");
|
|
4395
|
+
console.log(` Wallet: ${this.client.address}`);
|
|
4396
|
+
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
4397
|
+
console.log(" Opening the command center to fund your wallet...");
|
|
4398
|
+
console.log(` ${ccUrl}`);
|
|
4399
|
+
console.log("");
|
|
4400
|
+
openBrowser(ccUrl);
|
|
4401
|
+
console.log(" Waiting for ETH... (checking every 15s)");
|
|
4402
|
+
console.log(" Press Ctrl+C to exit.");
|
|
4403
|
+
console.log("");
|
|
4404
|
+
while (true) {
|
|
4405
|
+
await this.sleep(15e3);
|
|
4406
|
+
const balance = await publicClientInstance.getBalance({
|
|
4407
|
+
address: this.client.address
|
|
3862
4408
|
});
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
3868
|
-
console.log(" Opening the command center to fund your wallet...");
|
|
3869
|
-
console.log(` ${ccUrl}`);
|
|
3870
|
-
console.log("");
|
|
3871
|
-
openBrowser(ccUrl);
|
|
3872
|
-
console.log(" Waiting for ETH... (checking every 15s)");
|
|
3873
|
-
console.log(" Press Ctrl+C to exit.");
|
|
3874
|
-
console.log("");
|
|
3875
|
-
while (true) {
|
|
3876
|
-
await this.sleep(15e3);
|
|
3877
|
-
const balance = await publicClientInstance.getBalance({
|
|
3878
|
-
address: this.client.address
|
|
3879
|
-
});
|
|
3880
|
-
if (balance > BigInt(0)) {
|
|
3881
|
-
console.log(" ETH detected! Retrying config update...");
|
|
3882
|
-
console.log("");
|
|
4409
|
+
if (balance > BigInt(0)) {
|
|
4410
|
+
console.log(" ETH detected! Retrying config update...");
|
|
4411
|
+
console.log("");
|
|
4412
|
+
try {
|
|
3883
4413
|
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
4414
|
+
console.log(`Config updated on-chain`);
|
|
4415
|
+
this.pendingConfigHash = null;
|
|
4416
|
+
} catch (retryError) {
|
|
4417
|
+
const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
4418
|
+
console.warn(`Config update failed after funding: ${retryMsg}`);
|
|
4419
|
+
console.warn("Continuing with on-chain config.");
|
|
4420
|
+
this.configHash = onChainHash;
|
|
3887
4421
|
}
|
|
3888
|
-
|
|
4422
|
+
return;
|
|
3889
4423
|
}
|
|
3890
|
-
|
|
3891
|
-
console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
|
|
3892
|
-
this.configHash = onChainHash;
|
|
4424
|
+
process.stdout.write(".");
|
|
3893
4425
|
}
|
|
4426
|
+
} else {
|
|
4427
|
+
console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
|
|
4428
|
+
this.configHash = onChainHash;
|
|
3894
4429
|
}
|
|
3895
|
-
} else {
|
|
3896
|
-
const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
3897
|
-
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
3898
4430
|
}
|
|
3899
4431
|
}
|
|
3900
4432
|
/**
|
|
@@ -4028,6 +4560,10 @@ var AgentRuntime = class {
|
|
|
4028
4560
|
}
|
|
4029
4561
|
if (updated) {
|
|
4030
4562
|
this.riskManager = new RiskManager(this.config.trading);
|
|
4563
|
+
const savedRiskState = this.positionTracker.getRiskState();
|
|
4564
|
+
if (savedRiskState.lastResetDate) {
|
|
4565
|
+
this.riskManager.restoreState(savedRiskState);
|
|
4566
|
+
}
|
|
4031
4567
|
console.log("Risk params updated via command center");
|
|
4032
4568
|
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
4033
4569
|
this.relay?.sendMessage(
|
|
@@ -4221,16 +4757,44 @@ var AgentRuntime = class {
|
|
|
4221
4757
|
this.relay?.sendCommandResult(cmd.id, false, message);
|
|
4222
4758
|
}
|
|
4223
4759
|
}
|
|
4760
|
+
/**
|
|
4761
|
+
* Periodically check if the owner has approved the pending config hash.
|
|
4762
|
+
* Called from sendRelayStatus at most every 2.5 minutes (timestamp-throttled).
|
|
4763
|
+
*/
|
|
4764
|
+
async checkPendingConfigApproval() {
|
|
4765
|
+
if (!this.pendingConfigHash) return;
|
|
4766
|
+
try {
|
|
4767
|
+
const onChain = await this.client.registry.getConfigHash(BigInt(this.config.agentId));
|
|
4768
|
+
if (onChain === this.pendingConfigHash) {
|
|
4769
|
+
this.configHash = this.pendingConfigHash;
|
|
4770
|
+
this.pendingConfigHash = null;
|
|
4771
|
+
console.log("Config verified on-chain! Your agent will now appear on the leaderboard.");
|
|
4772
|
+
this.relay?.sendMessage(
|
|
4773
|
+
"config_updated",
|
|
4774
|
+
"success",
|
|
4775
|
+
"Config Verified",
|
|
4776
|
+
"Your LLM config has been verified on-chain. Your agent will now appear on the leaderboard."
|
|
4777
|
+
);
|
|
4778
|
+
}
|
|
4779
|
+
} catch {
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4224
4782
|
/**
|
|
4225
4783
|
* Send current status to the relay
|
|
4226
4784
|
*/
|
|
4227
4785
|
sendRelayStatus() {
|
|
4228
4786
|
if (!this.relay) return;
|
|
4787
|
+
const CONFIG_CHECK_INTERVAL_MS = 15e4;
|
|
4788
|
+
if (this.pendingConfigHash && Date.now() - this.lastConfigCheckAt >= CONFIG_CHECK_INTERVAL_MS) {
|
|
4789
|
+
this.lastConfigCheckAt = Date.now();
|
|
4790
|
+
this.checkPendingConfigApproval();
|
|
4791
|
+
}
|
|
4229
4792
|
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
4230
4793
|
const status = {
|
|
4231
4794
|
mode: this.mode,
|
|
4232
4795
|
agentId: String(this.config.agentId),
|
|
4233
4796
|
wallet: this.client?.address,
|
|
4797
|
+
sdkVersion: AGENT_VERSION,
|
|
4234
4798
|
cycleCount: this.cycleCount,
|
|
4235
4799
|
lastCycleAt: this.lastCycleAt,
|
|
4236
4800
|
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
@@ -4253,30 +4817,32 @@ var AgentRuntime = class {
|
|
|
4253
4817
|
perp: this.perpConnected ? {
|
|
4254
4818
|
enabled: true,
|
|
4255
4819
|
trading: this.perpTradingActive,
|
|
4256
|
-
equity:
|
|
4257
|
-
unrealizedPnl:
|
|
4258
|
-
marginUsed:
|
|
4259
|
-
openPositions:
|
|
4260
|
-
effectiveLeverage:
|
|
4820
|
+
equity: this.cachedPerpEquity,
|
|
4821
|
+
unrealizedPnl: this.cachedPerpUnrealizedPnl,
|
|
4822
|
+
marginUsed: this.cachedPerpMarginUsed,
|
|
4823
|
+
openPositions: this.cachedPerpOpenPositions,
|
|
4824
|
+
effectiveLeverage: this.cachedPerpLeverage,
|
|
4261
4825
|
pendingRecords: this.perpRecorder?.pendingRetries ?? 0
|
|
4262
|
-
} : void 0
|
|
4826
|
+
} : void 0,
|
|
4827
|
+
positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0,
|
|
4828
|
+
configHash: this.configHash || void 0,
|
|
4829
|
+
pendingConfigHash: this.pendingConfigHash
|
|
4830
|
+
// null preserved by JSON.stringify for clearing
|
|
4263
4831
|
};
|
|
4264
|
-
|
|
4832
|
+
this.relay.sendHeartbeat(status);
|
|
4833
|
+
if (this.perpConnected && this.perpPositions) {
|
|
4265
4834
|
this.perpPositions.getAccountSummary().then((account) => {
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
status.perp.effectiveLeverage = account.effectiveLeverage;
|
|
4271
|
-
}
|
|
4835
|
+
this.cachedPerpEquity = account.totalEquity;
|
|
4836
|
+
this.cachedPerpUnrealizedPnl = account.totalUnrealizedPnl;
|
|
4837
|
+
this.cachedPerpMarginUsed = account.totalMarginUsed;
|
|
4838
|
+
this.cachedPerpLeverage = account.effectiveLeverage;
|
|
4272
4839
|
}).catch(() => {
|
|
4273
4840
|
});
|
|
4274
4841
|
this.perpPositions.getPositionCount().then((count) => {
|
|
4275
|
-
|
|
4842
|
+
this.cachedPerpOpenPositions = count;
|
|
4276
4843
|
}).catch(() => {
|
|
4277
4844
|
});
|
|
4278
4845
|
}
|
|
4279
|
-
this.relay.sendHeartbeat(status);
|
|
4280
4846
|
}
|
|
4281
4847
|
/**
|
|
4282
4848
|
* Run a single trading cycle
|
|
@@ -4290,14 +4856,19 @@ var AgentRuntime = class {
|
|
|
4290
4856
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
4291
4857
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
4292
4858
|
this.lastPortfolioValue = marketData.portfolioValue;
|
|
4859
|
+
this.lastPrices = marketData.prices;
|
|
4293
4860
|
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4294
4861
|
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
4862
|
+
this.positionTracker.syncBalances(marketData.balances, marketData.prices);
|
|
4295
4863
|
const fundsOk = this.checkFundsLow(marketData);
|
|
4296
4864
|
if (!fundsOk) {
|
|
4297
4865
|
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
4298
4866
|
this.sendRelayStatus();
|
|
4299
4867
|
return;
|
|
4300
4868
|
}
|
|
4869
|
+
this.strategyContext.positions = this.positionTracker.getPositions();
|
|
4870
|
+
this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
|
|
4871
|
+
this.strategyContext.positionTracker = this.positionTracker;
|
|
4301
4872
|
let signals;
|
|
4302
4873
|
try {
|
|
4303
4874
|
signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
|
|
@@ -4363,13 +4934,30 @@ var AgentRuntime = class {
|
|
|
4363
4934
|
);
|
|
4364
4935
|
}
|
|
4365
4936
|
}
|
|
4937
|
+
for (const result of results) {
|
|
4938
|
+
const tokenIn = result.signal.tokenIn.toLowerCase();
|
|
4939
|
+
const tokenOut = result.signal.tokenOut.toLowerCase();
|
|
4940
|
+
this.positionTracker.recordTrade({
|
|
4941
|
+
action: result.signal.action,
|
|
4942
|
+
tokenIn,
|
|
4943
|
+
tokenOut,
|
|
4944
|
+
amountIn: result.signal.amountIn,
|
|
4945
|
+
priceIn: marketData.prices[tokenIn] || 0,
|
|
4946
|
+
priceOut: marketData.prices[tokenOut] || 0,
|
|
4947
|
+
txHash: result.txHash,
|
|
4948
|
+
reasoning: result.signal.reasoning,
|
|
4949
|
+
success: result.success
|
|
4950
|
+
});
|
|
4951
|
+
}
|
|
4366
4952
|
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
4367
4953
|
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
4368
4954
|
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
4369
4955
|
this.riskManager.updatePnL(marketPnL);
|
|
4956
|
+
this.positionTracker.saveRiskState(this.riskManager.exportState());
|
|
4370
4957
|
if (marketPnL !== 0) {
|
|
4371
4958
|
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
4372
4959
|
}
|
|
4960
|
+
this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
|
|
4373
4961
|
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
4374
4962
|
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4375
4963
|
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
@@ -4644,192 +5232,6 @@ var AgentRuntime = class {
|
|
|
4644
5232
|
|
|
4645
5233
|
// src/cli.ts
|
|
4646
5234
|
var import_accounts6 = require("viem/accounts");
|
|
4647
|
-
|
|
4648
|
-
// src/secure-env.ts
|
|
4649
|
-
var crypto = __toESM(require("crypto"));
|
|
4650
|
-
var fs = __toESM(require("fs"));
|
|
4651
|
-
var path = __toESM(require("path"));
|
|
4652
|
-
var ALGORITHM = "aes-256-gcm";
|
|
4653
|
-
var PBKDF2_ITERATIONS = 1e5;
|
|
4654
|
-
var SALT_LENGTH = 32;
|
|
4655
|
-
var IV_LENGTH = 16;
|
|
4656
|
-
var KEY_LENGTH = 32;
|
|
4657
|
-
var SENSITIVE_PATTERNS = [
|
|
4658
|
-
/PRIVATE_KEY$/i,
|
|
4659
|
-
/_API_KEY$/i,
|
|
4660
|
-
/API_KEY$/i,
|
|
4661
|
-
/_SECRET$/i,
|
|
4662
|
-
/^OPENAI_API_KEY$/i,
|
|
4663
|
-
/^ANTHROPIC_API_KEY$/i,
|
|
4664
|
-
/^GOOGLE_AI_API_KEY$/i,
|
|
4665
|
-
/^DEEPSEEK_API_KEY$/i,
|
|
4666
|
-
/^MISTRAL_API_KEY$/i,
|
|
4667
|
-
/^GROQ_API_KEY$/i,
|
|
4668
|
-
/^TOGETHER_API_KEY$/i
|
|
4669
|
-
];
|
|
4670
|
-
function isSensitiveKey(key) {
|
|
4671
|
-
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
4672
|
-
}
|
|
4673
|
-
function deriveKey(passphrase, salt) {
|
|
4674
|
-
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
4675
|
-
}
|
|
4676
|
-
function encryptValue(value, key) {
|
|
4677
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
4678
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
4679
|
-
let encrypted = cipher.update(value, "utf8", "hex");
|
|
4680
|
-
encrypted += cipher.final("hex");
|
|
4681
|
-
const tag = cipher.getAuthTag();
|
|
4682
|
-
return {
|
|
4683
|
-
iv: iv.toString("hex"),
|
|
4684
|
-
encrypted,
|
|
4685
|
-
tag: tag.toString("hex")
|
|
4686
|
-
};
|
|
4687
|
-
}
|
|
4688
|
-
function decryptValue(encrypted, key, iv, tag) {
|
|
4689
|
-
const decipher = crypto.createDecipheriv(
|
|
4690
|
-
ALGORITHM,
|
|
4691
|
-
key,
|
|
4692
|
-
Buffer.from(iv, "hex")
|
|
4693
|
-
);
|
|
4694
|
-
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
4695
|
-
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
4696
|
-
decrypted += decipher.final("utf8");
|
|
4697
|
-
return decrypted;
|
|
4698
|
-
}
|
|
4699
|
-
function parseEnvFile(content) {
|
|
4700
|
-
const entries = [];
|
|
4701
|
-
for (const line of content.split("\n")) {
|
|
4702
|
-
const trimmed = line.trim();
|
|
4703
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
4704
|
-
const eqIndex = trimmed.indexOf("=");
|
|
4705
|
-
if (eqIndex === -1) continue;
|
|
4706
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
4707
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
4708
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4709
|
-
value = value.slice(1, -1);
|
|
4710
|
-
}
|
|
4711
|
-
if (key && value) {
|
|
4712
|
-
entries.push({ key, value });
|
|
4713
|
-
}
|
|
4714
|
-
}
|
|
4715
|
-
return entries;
|
|
4716
|
-
}
|
|
4717
|
-
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
4718
|
-
if (!fs.existsSync(envPath)) {
|
|
4719
|
-
throw new Error(`File not found: ${envPath}`);
|
|
4720
|
-
}
|
|
4721
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
4722
|
-
const entries = parseEnvFile(content);
|
|
4723
|
-
if (entries.length === 0) {
|
|
4724
|
-
throw new Error("No environment variables found in file");
|
|
4725
|
-
}
|
|
4726
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
4727
|
-
const key = deriveKey(passphrase, salt);
|
|
4728
|
-
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
4729
|
-
if (isSensitiveKey(envKey)) {
|
|
4730
|
-
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
4731
|
-
return {
|
|
4732
|
-
key: envKey,
|
|
4733
|
-
value: encrypted,
|
|
4734
|
-
encrypted: true,
|
|
4735
|
-
iv,
|
|
4736
|
-
tag
|
|
4737
|
-
};
|
|
4738
|
-
}
|
|
4739
|
-
return {
|
|
4740
|
-
key: envKey,
|
|
4741
|
-
value,
|
|
4742
|
-
encrypted: false
|
|
4743
|
-
};
|
|
4744
|
-
});
|
|
4745
|
-
const encryptedEnv = {
|
|
4746
|
-
version: 1,
|
|
4747
|
-
salt: salt.toString("hex"),
|
|
4748
|
-
entries: encryptedEntries
|
|
4749
|
-
};
|
|
4750
|
-
const encPath = envPath + ".enc";
|
|
4751
|
-
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
4752
|
-
if (deleteOriginal) {
|
|
4753
|
-
fs.unlinkSync(envPath);
|
|
4754
|
-
}
|
|
4755
|
-
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
4756
|
-
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
4757
|
-
console.log(
|
|
4758
|
-
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
4759
|
-
);
|
|
4760
|
-
return encPath;
|
|
4761
|
-
}
|
|
4762
|
-
function decryptEnvFile(encPath, passphrase) {
|
|
4763
|
-
if (!fs.existsSync(encPath)) {
|
|
4764
|
-
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
4765
|
-
}
|
|
4766
|
-
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
4767
|
-
if (content.version !== 1) {
|
|
4768
|
-
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
4769
|
-
}
|
|
4770
|
-
const salt = Buffer.from(content.salt, "hex");
|
|
4771
|
-
const key = deriveKey(passphrase, salt);
|
|
4772
|
-
const result = {};
|
|
4773
|
-
for (const entry of content.entries) {
|
|
4774
|
-
if (entry.encrypted) {
|
|
4775
|
-
if (!entry.iv || !entry.tag) {
|
|
4776
|
-
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
4777
|
-
}
|
|
4778
|
-
try {
|
|
4779
|
-
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
4780
|
-
} catch {
|
|
4781
|
-
throw new Error(
|
|
4782
|
-
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
4783
|
-
);
|
|
4784
|
-
}
|
|
4785
|
-
} else {
|
|
4786
|
-
result[entry.key] = entry.value;
|
|
4787
|
-
}
|
|
4788
|
-
}
|
|
4789
|
-
return result;
|
|
4790
|
-
}
|
|
4791
|
-
function loadSecureEnv(basePath, passphrase) {
|
|
4792
|
-
const encPath = path.join(basePath, ".env.enc");
|
|
4793
|
-
const envPath = path.join(basePath, ".env");
|
|
4794
|
-
if (fs.existsSync(encPath)) {
|
|
4795
|
-
if (!passphrase) {
|
|
4796
|
-
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
4797
|
-
}
|
|
4798
|
-
if (!passphrase) {
|
|
4799
|
-
console.warn("");
|
|
4800
|
-
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
4801
|
-
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
4802
|
-
console.warn(" pass --passphrase when running the agent.");
|
|
4803
|
-
console.warn(" Falling back to plaintext .env file.");
|
|
4804
|
-
console.warn("");
|
|
4805
|
-
} else {
|
|
4806
|
-
const vars = decryptEnvFile(encPath, passphrase);
|
|
4807
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
4808
|
-
process.env[key] = value;
|
|
4809
|
-
}
|
|
4810
|
-
return true;
|
|
4811
|
-
}
|
|
4812
|
-
}
|
|
4813
|
-
if (fs.existsSync(envPath)) {
|
|
4814
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
4815
|
-
const entries = parseEnvFile(content);
|
|
4816
|
-
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
4817
|
-
if (sensitiveKeys.length > 0) {
|
|
4818
|
-
console.warn("");
|
|
4819
|
-
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
4820
|
-
for (const key of sensitiveKeys) {
|
|
4821
|
-
console.warn(` - ${key}`);
|
|
4822
|
-
}
|
|
4823
|
-
console.warn("");
|
|
4824
|
-
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
4825
|
-
console.warn("");
|
|
4826
|
-
}
|
|
4827
|
-
return false;
|
|
4828
|
-
}
|
|
4829
|
-
return false;
|
|
4830
|
-
}
|
|
4831
|
-
|
|
4832
|
-
// src/cli.ts
|
|
4833
5235
|
(0, import_dotenv2.config)();
|
|
4834
5236
|
var program = new import_commander.Command();
|
|
4835
5237
|
program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");
|