@exagent/agent 0.1.19 → 0.1.21
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-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/chunk-Z4N6G5XT.mjs +4897 -0
- package/dist/cli.js +1524 -1084
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +227 -5
- package/dist/index.d.ts +227 -5
- package/dist/index.js +933 -487
- package/dist/index.mjs +7 -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
|
};
|
|
@@ -1048,8 +1584,9 @@ var TradeExecutor = class {
|
|
|
1048
1584
|
return { success: true, txHash: result.hash };
|
|
1049
1585
|
} catch (error) {
|
|
1050
1586
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1051
|
-
|
|
1052
|
-
|
|
1587
|
+
const classified = classifyTradeError(message);
|
|
1588
|
+
console.error(`Trade failed (${classified.category}): ${classified.userMessage}`);
|
|
1589
|
+
return { success: false, error: classified.userMessage };
|
|
1053
1590
|
}
|
|
1054
1591
|
}
|
|
1055
1592
|
/**
|
|
@@ -1093,335 +1630,122 @@ var TradeExecutor = class {
|
|
|
1093
1630
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1094
1631
|
}
|
|
1095
1632
|
};
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
// USDC
|
|
1104
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
1105
|
-
// USDbC
|
|
1106
|
-
"0x4200000000000000000000000000000000000006": 18,
|
|
1107
|
-
// WETH
|
|
1108
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
1109
|
-
// DAI
|
|
1110
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
1111
|
-
// cbETH
|
|
1112
|
-
[NATIVE_ETH.toLowerCase()]: 18,
|
|
1113
|
-
// Native ETH
|
|
1114
|
-
// Base Mainnet — Established tokens
|
|
1115
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
1116
|
-
// AERO (Aerodrome)
|
|
1117
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
1118
|
-
// BRETT
|
|
1119
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
1120
|
-
// DEGEN
|
|
1121
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
1122
|
-
// VIRTUAL
|
|
1123
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
1124
|
-
// TOSHI
|
|
1125
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
1126
|
-
// cbBTC
|
|
1127
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
1128
|
-
// ezETH (Renzo)
|
|
1129
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
1130
|
-
// wstETH (Lido)
|
|
1131
|
-
};
|
|
1132
|
-
function getTokenDecimals(address) {
|
|
1133
|
-
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
1134
|
-
if (decimals === void 0) {
|
|
1135
|
-
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
1136
|
-
return 18;
|
|
1633
|
+
function classifyTradeError(message) {
|
|
1634
|
+
const lower = message.toLowerCase();
|
|
1635
|
+
if (lower.includes("configmismatch") || lower.includes("config mismatch")) {
|
|
1636
|
+
return {
|
|
1637
|
+
category: "config_mismatch",
|
|
1638
|
+
userMessage: "Config hash mismatch \u2014 on-chain config does not match. Restart the agent to re-sync."
|
|
1639
|
+
};
|
|
1137
1640
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
// WETH
|
|
1144
|
-
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
1145
|
-
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
1146
|
-
// USDC
|
|
1147
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
1148
|
-
// USDbC
|
|
1149
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
1150
|
-
// cbETH
|
|
1151
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
1152
|
-
// DAI
|
|
1153
|
-
// Established
|
|
1154
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
1155
|
-
// AERO
|
|
1156
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
1157
|
-
// BRETT
|
|
1158
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
1159
|
-
// DEGEN
|
|
1160
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
1161
|
-
// VIRTUAL
|
|
1162
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
1163
|
-
// TOSHI
|
|
1164
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
1165
|
-
// cbBTC
|
|
1166
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
1167
|
-
// ezETH
|
|
1168
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
1169
|
-
// wstETH
|
|
1170
|
-
};
|
|
1171
|
-
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
1172
|
-
var PRICE_STALENESS_MS = 6e4;
|
|
1173
|
-
var MarketDataService = class {
|
|
1174
|
-
rpcUrl;
|
|
1175
|
-
client;
|
|
1176
|
-
/** Cached prices from last fetch */
|
|
1177
|
-
cachedPrices = {};
|
|
1178
|
-
/** Timestamp of last successful price fetch */
|
|
1179
|
-
lastPriceFetchAt = 0;
|
|
1180
|
-
constructor(rpcUrl) {
|
|
1181
|
-
this.rpcUrl = rpcUrl;
|
|
1182
|
-
this.client = (0, import_viem.createPublicClient)({
|
|
1183
|
-
transport: (0, import_viem.http)(rpcUrl)
|
|
1184
|
-
});
|
|
1641
|
+
if (lower.includes("insufficient funds") || lower.includes("exceeds the balance")) {
|
|
1642
|
+
return {
|
|
1643
|
+
category: "insufficient_funds",
|
|
1644
|
+
userMessage: "Insufficient ETH for gas \u2014 fund your trading wallet."
|
|
1645
|
+
};
|
|
1185
1646
|
}
|
|
1186
|
-
|
|
1187
|
-
cachedVolume24h = {};
|
|
1188
|
-
/** Cached price change data */
|
|
1189
|
-
cachedPriceChange24h = {};
|
|
1190
|
-
/**
|
|
1191
|
-
* Fetch current market data for the agent
|
|
1192
|
-
*/
|
|
1193
|
-
async fetchMarketData(walletAddress, tokenAddresses) {
|
|
1194
|
-
const prices = await this.fetchPrices(tokenAddresses);
|
|
1195
|
-
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
1196
|
-
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
1197
|
-
let gasPrice;
|
|
1198
|
-
try {
|
|
1199
|
-
gasPrice = await this.client.getGasPrice();
|
|
1200
|
-
} catch {
|
|
1201
|
-
}
|
|
1647
|
+
if (lower.includes("out of gas") || lower.includes("intrinsic gas too low")) {
|
|
1202
1648
|
return {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
balances,
|
|
1206
|
-
portfolioValue,
|
|
1207
|
-
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1208
|
-
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1209
|
-
gasPrice,
|
|
1210
|
-
network: {
|
|
1211
|
-
chainId: this.client.chain?.id ?? 8453
|
|
1212
|
-
}
|
|
1649
|
+
category: "out_of_gas",
|
|
1650
|
+
userMessage: "Transaction ran out of gas during execution. This is usually transient \u2014 retrying with more gas."
|
|
1213
1651
|
};
|
|
1214
1652
|
}
|
|
1653
|
+
if (lower.includes("minamountnotmet") || lower.includes("min amount") || lower.includes("slippage")) {
|
|
1654
|
+
return {
|
|
1655
|
+
category: "slippage",
|
|
1656
|
+
userMessage: "Swap failed due to slippage \u2014 price moved beyond tolerance. Will retry next cycle."
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
if (lower.includes("notauthorizedforagent") || lower.includes("not authorized")) {
|
|
1660
|
+
return {
|
|
1661
|
+
category: "not_authorized",
|
|
1662
|
+
userMessage: "Wallet not authorized for this agent. Ensure your wallet is linked via the registry."
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
if (lower.includes("aggregatornotwhitelisted")) {
|
|
1666
|
+
return {
|
|
1667
|
+
category: "aggregator_error",
|
|
1668
|
+
userMessage: "DEX aggregator not whitelisted on the router. Contact support."
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
if (lower.includes("reverted") || lower.includes("execution reverted")) {
|
|
1672
|
+
return {
|
|
1673
|
+
category: "reverted",
|
|
1674
|
+
userMessage: `Transaction reverted: ${message.slice(0, 200)}`
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
|
|
1678
|
+
return {
|
|
1679
|
+
category: "network",
|
|
1680
|
+
userMessage: "Network error \u2014 RPC node may be down. Will retry next cycle."
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
category: "unknown",
|
|
1685
|
+
userMessage: message.slice(0, 300)
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
|
|
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;
|
|
1700
|
+
}
|
|
1215
1701
|
/**
|
|
1216
|
-
*
|
|
1702
|
+
* Filter signals through risk checks
|
|
1703
|
+
* Returns only signals that pass all guardrails
|
|
1217
1704
|
*/
|
|
1218
|
-
|
|
1219
|
-
|
|
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;
|
|
1711
|
+
}
|
|
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));
|
|
1220
1717
|
}
|
|
1221
1718
|
/**
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
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.
|
|
1224
1722
|
*/
|
|
1225
|
-
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
1228
|
-
for (const addr of tokenAddresses) {
|
|
1229
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1230
|
-
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1231
|
-
prices2[addr.toLowerCase()] = 1;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
return prices2;
|
|
1723
|
+
validateSignal(signal, marketData) {
|
|
1724
|
+
if (signal.action === "hold") {
|
|
1725
|
+
return true;
|
|
1235
1726
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1240
|
-
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1241
|
-
idsToFetch.add(cgId);
|
|
1242
|
-
}
|
|
1727
|
+
if (signal.confidence < 0.5) {
|
|
1728
|
+
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1729
|
+
return false;
|
|
1243
1730
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
try {
|
|
1247
|
-
const ids = Array.from(idsToFetch).join(",");
|
|
1248
|
-
const response = await fetch(
|
|
1249
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1250
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1251
|
-
);
|
|
1252
|
-
if (response.ok) {
|
|
1253
|
-
const data = await response.json();
|
|
1254
|
-
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1255
|
-
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1256
|
-
if (id === cgId) {
|
|
1257
|
-
const key = addr.toLowerCase();
|
|
1258
|
-
prices[key] = priceData.usd;
|
|
1259
|
-
if (priceData.usd_24h_vol !== void 0) {
|
|
1260
|
-
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1261
|
-
}
|
|
1262
|
-
if (priceData.usd_24h_change !== void 0) {
|
|
1263
|
-
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
this.lastPriceFetchAt = Date.now();
|
|
1269
|
-
} else {
|
|
1270
|
-
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1271
|
-
}
|
|
1272
|
-
} catch (error) {
|
|
1273
|
-
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1274
|
-
}
|
|
1731
|
+
if (signal.action === "sell") {
|
|
1732
|
+
return true;
|
|
1275
1733
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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;
|
|
1281
1741
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1288
|
-
const llamaResponse = await fetch(
|
|
1289
|
-
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1290
|
-
{ 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`
|
|
1291
1747
|
);
|
|
1292
|
-
|
|
1293
|
-
const llamaData = await llamaResponse.json();
|
|
1294
|
-
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1295
|
-
const addr = key.replace("base:", "").toLowerCase();
|
|
1296
|
-
if (data.price && data.confidence > 0.5) {
|
|
1297
|
-
prices[addr] = data.price;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1301
|
-
}
|
|
1302
|
-
} catch {
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
if (Object.keys(prices).length > 0) {
|
|
1306
|
-
this.cachedPrices = prices;
|
|
1307
|
-
}
|
|
1308
|
-
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1309
|
-
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1310
|
-
return { ...this.cachedPrices };
|
|
1311
|
-
}
|
|
1312
|
-
for (const addr of tokenAddresses) {
|
|
1313
|
-
if (!prices[addr.toLowerCase()]) {
|
|
1314
|
-
console.warn(`No price available for ${addr}, using 0`);
|
|
1315
|
-
prices[addr.toLowerCase()] = 0;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
return prices;
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Fetch real on-chain balances: native ETH + ERC-20 tokens
|
|
1322
|
-
*/
|
|
1323
|
-
async fetchBalances(walletAddress, tokenAddresses) {
|
|
1324
|
-
const balances = {};
|
|
1325
|
-
const wallet = walletAddress;
|
|
1326
|
-
try {
|
|
1327
|
-
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
1328
|
-
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
1329
|
-
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
1330
|
-
try {
|
|
1331
|
-
const balance = await this.client.readContract({
|
|
1332
|
-
address: tokenAddress,
|
|
1333
|
-
abi: import_viem.erc20Abi,
|
|
1334
|
-
functionName: "balanceOf",
|
|
1335
|
-
args: [wallet]
|
|
1336
|
-
});
|
|
1337
|
-
return { address: tokenAddress.toLowerCase(), balance };
|
|
1338
|
-
} catch (error) {
|
|
1339
|
-
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
1340
|
-
}
|
|
1341
|
-
});
|
|
1342
|
-
const results = await Promise.all(erc20Promises);
|
|
1343
|
-
for (const { address, balance } of results) {
|
|
1344
|
-
balances[address] = balance;
|
|
1345
|
-
}
|
|
1346
|
-
} catch (error) {
|
|
1347
|
-
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
1348
|
-
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
1349
|
-
for (const address of tokenAddresses) {
|
|
1350
|
-
balances[address.toLowerCase()] = 0n;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return balances;
|
|
1354
|
-
}
|
|
1355
|
-
/**
|
|
1356
|
-
* Calculate total portfolio value in USD
|
|
1357
|
-
*/
|
|
1358
|
-
calculatePortfolioValue(balances, prices) {
|
|
1359
|
-
let total = 0;
|
|
1360
|
-
for (const [address, balance] of Object.entries(balances)) {
|
|
1361
|
-
const price = prices[address.toLowerCase()] || 0;
|
|
1362
|
-
const decimals = getTokenDecimals(address);
|
|
1363
|
-
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1364
|
-
total += amount * price;
|
|
1365
|
-
}
|
|
1366
|
-
return total;
|
|
1367
|
-
}
|
|
1368
|
-
};
|
|
1369
|
-
|
|
1370
|
-
// src/trading/risk.ts
|
|
1371
|
-
var RiskManager = class {
|
|
1372
|
-
config;
|
|
1373
|
-
dailyPnL = 0;
|
|
1374
|
-
dailyFees = 0;
|
|
1375
|
-
lastResetDate = "";
|
|
1376
|
-
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1377
|
-
minTradeValueUSD;
|
|
1378
|
-
constructor(config) {
|
|
1379
|
-
this.config = config;
|
|
1380
|
-
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Filter signals through risk checks
|
|
1384
|
-
* Returns only signals that pass all guardrails
|
|
1385
|
-
*/
|
|
1386
|
-
filterSignals(signals, marketData) {
|
|
1387
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1388
|
-
if (today !== this.lastResetDate) {
|
|
1389
|
-
this.dailyPnL = 0;
|
|
1390
|
-
this.dailyFees = 0;
|
|
1391
|
-
this.lastResetDate = today;
|
|
1392
|
-
}
|
|
1393
|
-
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1394
|
-
console.warn("Daily loss limit reached - no new trades");
|
|
1395
|
-
return [];
|
|
1396
|
-
}
|
|
1397
|
-
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Validate individual signal against risk limits
|
|
1401
|
-
*/
|
|
1402
|
-
validateSignal(signal, marketData) {
|
|
1403
|
-
if (signal.action === "hold") {
|
|
1404
|
-
return true;
|
|
1405
|
-
}
|
|
1406
|
-
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1407
|
-
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1408
|
-
if (signalValue > maxPositionValue) {
|
|
1409
|
-
console.warn(
|
|
1410
|
-
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1411
|
-
);
|
|
1412
|
-
return false;
|
|
1413
|
-
}
|
|
1414
|
-
if (signal.confidence < 0.5) {
|
|
1415
|
-
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1416
|
-
return false;
|
|
1417
|
-
}
|
|
1418
|
-
if (signal.action === "buy" && this.config.maxConcurrentPositions) {
|
|
1419
|
-
const activePositions = this.countActivePositions(marketData);
|
|
1420
|
-
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1421
|
-
console.warn(
|
|
1422
|
-
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1423
|
-
);
|
|
1424
|
-
return false;
|
|
1748
|
+
return false;
|
|
1425
1749
|
}
|
|
1426
1750
|
}
|
|
1427
1751
|
if (signalValue < this.minTradeValueUSD) {
|
|
@@ -1472,6 +1796,31 @@ var RiskManager = class {
|
|
|
1472
1796
|
updateFees(fees) {
|
|
1473
1797
|
this.dailyFees += fees;
|
|
1474
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
|
+
}
|
|
1475
1824
|
/**
|
|
1476
1825
|
* Get current risk status
|
|
1477
1826
|
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
@@ -1585,6 +1934,55 @@ var RiskManager = class {
|
|
|
1585
1934
|
}
|
|
1586
1935
|
};
|
|
1587
1936
|
|
|
1937
|
+
// src/store.ts
|
|
1938
|
+
var import_fs3 = require("fs");
|
|
1939
|
+
var import_path3 = require("path");
|
|
1940
|
+
var FileStore = class {
|
|
1941
|
+
data = {};
|
|
1942
|
+
filePath;
|
|
1943
|
+
dirty = false;
|
|
1944
|
+
constructor(dataDir) {
|
|
1945
|
+
const dir = dataDir || (0, import_path3.join)(process.cwd(), "data");
|
|
1946
|
+
this.filePath = (0, import_path3.join)(dir, "strategy-store.json");
|
|
1947
|
+
if (!(0, import_fs3.existsSync)(dir)) {
|
|
1948
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
1949
|
+
}
|
|
1950
|
+
if ((0, import_fs3.existsSync)(this.filePath)) {
|
|
1951
|
+
try {
|
|
1952
|
+
const raw = (0, import_fs3.readFileSync)(this.filePath, "utf-8");
|
|
1953
|
+
this.data = JSON.parse(raw);
|
|
1954
|
+
} catch {
|
|
1955
|
+
this.data = {};
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
get(key) {
|
|
1960
|
+
return this.data[key];
|
|
1961
|
+
}
|
|
1962
|
+
set(key, value) {
|
|
1963
|
+
this.data[key] = value;
|
|
1964
|
+
this.dirty = true;
|
|
1965
|
+
this.flush();
|
|
1966
|
+
}
|
|
1967
|
+
delete(key) {
|
|
1968
|
+
delete this.data[key];
|
|
1969
|
+
this.dirty = true;
|
|
1970
|
+
this.flush();
|
|
1971
|
+
}
|
|
1972
|
+
keys() {
|
|
1973
|
+
return Object.keys(this.data);
|
|
1974
|
+
}
|
|
1975
|
+
flush() {
|
|
1976
|
+
if (!this.dirty) return;
|
|
1977
|
+
try {
|
|
1978
|
+
(0, import_fs3.writeFileSync)(this.filePath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
1979
|
+
this.dirty = false;
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
console.warn("Failed to persist strategy store:", error instanceof Error ? error.message : error);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1588
1986
|
// src/vault/manager.ts
|
|
1589
1987
|
var import_viem2 = require("viem");
|
|
1590
1988
|
var import_accounts = require("viem/accounts");
|
|
@@ -1884,292 +2282,29 @@ var VaultManager = class {
|
|
|
1884
2282
|
};
|
|
1885
2283
|
|
|
1886
2284
|
// src/relay.ts
|
|
1887
|
-
var
|
|
1888
|
-
var
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
reconnectAttempts = 0;
|
|
1896
|
-
maxReconnectAttempts = 50;
|
|
1897
|
-
reconnectTimer = null;
|
|
1898
|
-
heartbeatTimer = null;
|
|
1899
|
-
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();
|
|
1900
2293
|
constructor(config) {
|
|
1901
|
-
this.
|
|
2294
|
+
this.apiUrl = config.apiUrl;
|
|
1902
2295
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
console.error("Relay: Failed to create WebSocket:", error);
|
|
1914
|
-
this.scheduleReconnect();
|
|
1915
|
-
reject(error);
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
const connectTimeout = setTimeout(() => {
|
|
1919
|
-
if (!this.authenticated) {
|
|
1920
|
-
console.error("Relay: Connection timeout");
|
|
1921
|
-
this.ws?.close();
|
|
1922
|
-
this.scheduleReconnect();
|
|
1923
|
-
reject(new Error("Connection timeout"));
|
|
1924
|
-
}
|
|
1925
|
-
}, 15e3);
|
|
1926
|
-
this.ws.on("open", async () => {
|
|
1927
|
-
this.authRejected = false;
|
|
1928
|
-
console.log("Relay: Connected, authenticating...");
|
|
1929
|
-
try {
|
|
1930
|
-
await this.authenticate();
|
|
1931
|
-
} catch (error) {
|
|
1932
|
-
console.error("Relay: Authentication failed:", error);
|
|
1933
|
-
this.ws?.close();
|
|
1934
|
-
clearTimeout(connectTimeout);
|
|
1935
|
-
reject(error);
|
|
1936
|
-
}
|
|
1937
|
-
});
|
|
1938
|
-
this.ws.on("message", (raw) => {
|
|
1939
|
-
try {
|
|
1940
|
-
const data = JSON.parse(raw.toString());
|
|
1941
|
-
this.handleMessage(data);
|
|
1942
|
-
if (data.type === "auth_success") {
|
|
1943
|
-
clearTimeout(connectTimeout);
|
|
1944
|
-
this.authenticated = true;
|
|
1945
|
-
this.reconnectAttempts = 0;
|
|
1946
|
-
this.startHeartbeat();
|
|
1947
|
-
console.log("Relay: Authenticated successfully");
|
|
1948
|
-
resolve();
|
|
1949
|
-
} else if (data.type === "auth_error") {
|
|
1950
|
-
clearTimeout(connectTimeout);
|
|
1951
|
-
this.authRejected = true;
|
|
1952
|
-
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
1953
|
-
reject(new Error(data.message));
|
|
1954
|
-
}
|
|
1955
|
-
} catch {
|
|
1956
|
-
}
|
|
1957
|
-
});
|
|
1958
|
-
this.ws.on("close", (code, reason) => {
|
|
1959
|
-
clearTimeout(connectTimeout);
|
|
1960
|
-
this.authenticated = false;
|
|
1961
|
-
this.stopHeartbeat();
|
|
1962
|
-
if (!this.stopped) {
|
|
1963
|
-
if (!this.authRejected) {
|
|
1964
|
-
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
1965
|
-
}
|
|
1966
|
-
this.scheduleReconnect();
|
|
1967
|
-
}
|
|
1968
|
-
});
|
|
1969
|
-
this.ws.on("error", (error) => {
|
|
1970
|
-
if (!this.stopped) {
|
|
1971
|
-
console.error("Relay: WebSocket error:", error.message);
|
|
1972
|
-
}
|
|
1973
|
-
});
|
|
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);
|
|
1974
2306
|
});
|
|
1975
|
-
|
|
1976
|
-
/**
|
|
1977
|
-
* Authenticate with the relay server using wallet signature
|
|
1978
|
-
*/
|
|
1979
|
-
async authenticate() {
|
|
1980
|
-
const account = (0, import_accounts2.privateKeyToAccount)(this.config.privateKey);
|
|
1981
|
-
const timestamp = Math.floor(Date.now() / 1e3);
|
|
1982
|
-
const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
|
|
1983
|
-
const signature = await (0, import_accounts2.signMessage)({
|
|
1984
|
-
message,
|
|
1985
|
-
privateKey: this.config.privateKey
|
|
1986
|
-
});
|
|
1987
|
-
this.send({
|
|
1988
|
-
type: "auth",
|
|
1989
|
-
agentId: this.config.agentId,
|
|
1990
|
-
wallet: account.address,
|
|
1991
|
-
timestamp,
|
|
1992
|
-
signature,
|
|
1993
|
-
sdkVersion: import_sdk.SDK_VERSION
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
/**
|
|
1997
|
-
* Handle incoming messages from the relay server
|
|
1998
|
-
*/
|
|
1999
|
-
handleMessage(data) {
|
|
2000
|
-
switch (data.type) {
|
|
2001
|
-
case "command":
|
|
2002
|
-
if (data.command && this.config.onCommand) {
|
|
2003
|
-
this.config.onCommand(data.command);
|
|
2004
|
-
}
|
|
2005
|
-
break;
|
|
2006
|
-
case "auth_success":
|
|
2007
|
-
case "auth_error":
|
|
2008
|
-
break;
|
|
2009
|
-
case "error":
|
|
2010
|
-
console.error(`Relay: Server error: ${data.message}`);
|
|
2011
|
-
break;
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
/**
|
|
2015
|
-
* Send a status heartbeat
|
|
2016
|
-
*/
|
|
2017
|
-
sendHeartbeat(status) {
|
|
2018
|
-
if (!this.authenticated) return;
|
|
2019
|
-
this.send({
|
|
2020
|
-
type: "heartbeat",
|
|
2021
|
-
agentId: this.config.agentId,
|
|
2022
|
-
status
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
/**
|
|
2026
|
-
* Send a status update (outside of regular heartbeat)
|
|
2027
|
-
*/
|
|
2028
|
-
sendStatusUpdate(status) {
|
|
2029
|
-
if (!this.authenticated) return;
|
|
2030
|
-
this.send({
|
|
2031
|
-
type: "status_update",
|
|
2032
|
-
agentId: this.config.agentId,
|
|
2033
|
-
status
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
/**
|
|
2037
|
-
* Send a message to the command center
|
|
2038
|
-
*/
|
|
2039
|
-
sendMessage(messageType, level, title, body, data) {
|
|
2040
|
-
if (!this.authenticated) return;
|
|
2041
|
-
this.send({
|
|
2042
|
-
type: "message",
|
|
2043
|
-
agentId: this.config.agentId,
|
|
2044
|
-
messageType,
|
|
2045
|
-
level,
|
|
2046
|
-
title,
|
|
2047
|
-
body,
|
|
2048
|
-
data
|
|
2049
|
-
});
|
|
2050
|
-
}
|
|
2051
|
-
/**
|
|
2052
|
-
* Send a command execution result
|
|
2053
|
-
*/
|
|
2054
|
-
sendCommandResult(commandId, success, result) {
|
|
2055
|
-
if (!this.authenticated) return;
|
|
2056
|
-
this.send({
|
|
2057
|
-
type: "command_result",
|
|
2058
|
-
agentId: this.config.agentId,
|
|
2059
|
-
commandId,
|
|
2060
|
-
success,
|
|
2061
|
-
result
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
/**
|
|
2065
|
-
* Start the heartbeat timer
|
|
2066
|
-
*/
|
|
2067
|
-
startHeartbeat() {
|
|
2068
|
-
this.stopHeartbeat();
|
|
2069
|
-
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
2070
|
-
this.heartbeatTimer = setInterval(() => {
|
|
2071
|
-
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2072
|
-
this.ws.ping();
|
|
2073
|
-
}
|
|
2074
|
-
}, interval);
|
|
2075
|
-
}
|
|
2076
|
-
/**
|
|
2077
|
-
* Stop the heartbeat timer
|
|
2078
|
-
*/
|
|
2079
|
-
stopHeartbeat() {
|
|
2080
|
-
if (this.heartbeatTimer) {
|
|
2081
|
-
clearInterval(this.heartbeatTimer);
|
|
2082
|
-
this.heartbeatTimer = null;
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Schedule a reconnection with exponential backoff
|
|
2087
|
-
*/
|
|
2088
|
-
scheduleReconnect() {
|
|
2089
|
-
if (this.stopped) return;
|
|
2090
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
2091
|
-
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
2095
|
-
this.reconnectAttempts++;
|
|
2096
|
-
console.log(
|
|
2097
|
-
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
2098
|
-
);
|
|
2099
|
-
this.reconnectTimer = setTimeout(() => {
|
|
2100
|
-
this.connect().catch(() => {
|
|
2101
|
-
});
|
|
2102
|
-
}, delay);
|
|
2103
|
-
}
|
|
2104
|
-
/**
|
|
2105
|
-
* Send a JSON message to the WebSocket
|
|
2106
|
-
*/
|
|
2107
|
-
send(data) {
|
|
2108
|
-
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2109
|
-
this.ws.send(JSON.stringify(data));
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
/**
|
|
2113
|
-
* Check if connected and authenticated
|
|
2114
|
-
*/
|
|
2115
|
-
get isConnected() {
|
|
2116
|
-
return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
|
|
2117
|
-
}
|
|
2118
|
-
/**
|
|
2119
|
-
* Disconnect and stop reconnecting
|
|
2120
|
-
*/
|
|
2121
|
-
disconnect() {
|
|
2122
|
-
this.stopped = true;
|
|
2123
|
-
this.stopHeartbeat();
|
|
2124
|
-
if (this.reconnectTimer) {
|
|
2125
|
-
clearTimeout(this.reconnectTimer);
|
|
2126
|
-
this.reconnectTimer = null;
|
|
2127
|
-
}
|
|
2128
|
-
if (this.ws) {
|
|
2129
|
-
this.ws.close(1e3, "Agent shutting down");
|
|
2130
|
-
this.ws = null;
|
|
2131
|
-
}
|
|
2132
|
-
this.authenticated = false;
|
|
2133
|
-
console.log("Relay: Disconnected");
|
|
2134
|
-
}
|
|
2135
|
-
};
|
|
2136
|
-
|
|
2137
|
-
// src/browser-open.ts
|
|
2138
|
-
var import_child_process2 = require("child_process");
|
|
2139
|
-
function openBrowser(url) {
|
|
2140
|
-
const platform = process.platform;
|
|
2141
|
-
try {
|
|
2142
|
-
if (platform === "darwin") {
|
|
2143
|
-
(0, import_child_process2.exec)(`open "${url}"`);
|
|
2144
|
-
} else if (platform === "win32") {
|
|
2145
|
-
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
2146
|
-
} else {
|
|
2147
|
-
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
2148
|
-
}
|
|
2149
|
-
} catch {
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
// src/perp/client.ts
|
|
2154
|
-
var HyperliquidClient = class {
|
|
2155
|
-
apiUrl;
|
|
2156
|
-
meta = null;
|
|
2157
|
-
assetIndexCache = /* @__PURE__ */ new Map();
|
|
2158
|
-
constructor(config) {
|
|
2159
|
-
this.apiUrl = config.apiUrl;
|
|
2160
|
-
}
|
|
2161
|
-
// ============================================================
|
|
2162
|
-
// INFO API (read-only)
|
|
2163
|
-
// ============================================================
|
|
2164
|
-
/** Fetch perpetuals metadata (asset specs, names, indices) */
|
|
2165
|
-
async getMeta() {
|
|
2166
|
-
if (this.meta) return this.meta;
|
|
2167
|
-
const resp = await this.infoRequest({ type: "meta" });
|
|
2168
|
-
this.meta = resp.universe;
|
|
2169
|
-
this.meta.forEach((asset, idx) => {
|
|
2170
|
-
this.assetIndexCache.set(asset.name, idx);
|
|
2171
|
-
});
|
|
2172
|
-
return this.meta;
|
|
2307
|
+
return this.meta;
|
|
2173
2308
|
}
|
|
2174
2309
|
/** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
|
|
2175
2310
|
async getAssetIndex(coin) {
|
|
@@ -2754,7 +2889,7 @@ var PositionManager = class {
|
|
|
2754
2889
|
};
|
|
2755
2890
|
|
|
2756
2891
|
// src/perp/websocket.ts
|
|
2757
|
-
var
|
|
2892
|
+
var import_ws = __toESM(require("ws"));
|
|
2758
2893
|
var HyperliquidWebSocket = class {
|
|
2759
2894
|
wsUrl;
|
|
2760
2895
|
userAddress;
|
|
@@ -2786,14 +2921,14 @@ var HyperliquidWebSocket = class {
|
|
|
2786
2921
|
* Connect to Hyperliquid WebSocket and subscribe to user events.
|
|
2787
2922
|
*/
|
|
2788
2923
|
async connect() {
|
|
2789
|
-
if (this.ws?.readyState ===
|
|
2924
|
+
if (this.ws?.readyState === import_ws.default.OPEN || this.isConnecting) {
|
|
2790
2925
|
return;
|
|
2791
2926
|
}
|
|
2792
2927
|
this.isConnecting = true;
|
|
2793
2928
|
this.shouldReconnect = true;
|
|
2794
2929
|
return new Promise((resolve, reject) => {
|
|
2795
2930
|
try {
|
|
2796
|
-
this.ws = new
|
|
2931
|
+
this.ws = new import_ws.default(this.wsUrl);
|
|
2797
2932
|
this.ws.on("open", () => {
|
|
2798
2933
|
this.isConnecting = false;
|
|
2799
2934
|
this.reconnectAttempts = 0;
|
|
@@ -2846,7 +2981,7 @@ var HyperliquidWebSocket = class {
|
|
|
2846
2981
|
this.stopPing();
|
|
2847
2982
|
if (this.ws) {
|
|
2848
2983
|
this.ws.removeAllListeners();
|
|
2849
|
-
if (this.ws.readyState ===
|
|
2984
|
+
if (this.ws.readyState === import_ws.default.OPEN) {
|
|
2850
2985
|
this.ws.close(1e3, "Client disconnect");
|
|
2851
2986
|
}
|
|
2852
2987
|
this.ws = null;
|
|
@@ -2857,7 +2992,7 @@ var HyperliquidWebSocket = class {
|
|
|
2857
2992
|
* Check if WebSocket is connected.
|
|
2858
2993
|
*/
|
|
2859
2994
|
get isConnected() {
|
|
2860
|
-
return this.ws?.readyState ===
|
|
2995
|
+
return this.ws?.readyState === import_ws.default.OPEN;
|
|
2861
2996
|
}
|
|
2862
2997
|
// ============================================================
|
|
2863
2998
|
// EVENT HANDLERS
|
|
@@ -2984,7 +3119,7 @@ var HyperliquidWebSocket = class {
|
|
|
2984
3119
|
startPing() {
|
|
2985
3120
|
this.stopPing();
|
|
2986
3121
|
this.pingTimer = setInterval(() => {
|
|
2987
|
-
if (this.ws?.readyState ===
|
|
3122
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2988
3123
|
this.ws.send(JSON.stringify({ method: "ping" }));
|
|
2989
3124
|
}
|
|
2990
3125
|
}, 25e3);
|
|
@@ -2999,7 +3134,7 @@ var HyperliquidWebSocket = class {
|
|
|
2999
3134
|
// HELPERS
|
|
3000
3135
|
// ============================================================
|
|
3001
3136
|
subscribe(msg) {
|
|
3002
|
-
if (this.ws?.readyState ===
|
|
3137
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
3003
3138
|
this.ws.send(JSON.stringify(msg));
|
|
3004
3139
|
}
|
|
3005
3140
|
}
|
|
@@ -3008,7 +3143,7 @@ var HyperliquidWebSocket = class {
|
|
|
3008
3143
|
// src/perp/recorder.ts
|
|
3009
3144
|
var import_viem4 = require("viem");
|
|
3010
3145
|
var import_chains2 = require("viem/chains");
|
|
3011
|
-
var
|
|
3146
|
+
var import_accounts2 = require("viem/accounts");
|
|
3012
3147
|
var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
|
|
3013
3148
|
var ROUTER_ABI = [
|
|
3014
3149
|
{
|
|
@@ -3047,7 +3182,7 @@ var PerpTradeRecorder = class {
|
|
|
3047
3182
|
constructor(opts) {
|
|
3048
3183
|
this.agentId = opts.agentId;
|
|
3049
3184
|
this.configHash = opts.configHash;
|
|
3050
|
-
this.account = (0,
|
|
3185
|
+
this.account = (0, import_accounts2.privateKeyToAccount)(opts.privateKey);
|
|
3051
3186
|
const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
|
|
3052
3187
|
const transport = (0, import_viem4.http)(rpcUrl);
|
|
3053
3188
|
this.publicClient = (0, import_viem4.createPublicClient)({
|
|
@@ -3155,232 +3290,683 @@ var PerpTradeRecorder = class {
|
|
|
3155
3290
|
}
|
|
3156
3291
|
}
|
|
3157
3292
|
/**
|
|
3158
|
-
* Process the retry queue — attempt to re-submit failed recordings.
|
|
3293
|
+
* Process the retry queue — attempt to re-submit failed recordings.
|
|
3294
|
+
*/
|
|
3295
|
+
async processRetryQueue() {
|
|
3296
|
+
if (this.retryQueue.length === 0) return;
|
|
3297
|
+
const now = Date.now();
|
|
3298
|
+
const toRetry = this.retryQueue.filter(
|
|
3299
|
+
(item) => now - item.lastAttempt >= RETRY_DELAY_MS
|
|
3300
|
+
);
|
|
3301
|
+
for (const item of toRetry) {
|
|
3302
|
+
item.retries++;
|
|
3303
|
+
item.lastAttempt = now;
|
|
3304
|
+
if (item.retries > MAX_RETRIES) {
|
|
3305
|
+
console.error(
|
|
3306
|
+
`Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
|
|
3307
|
+
);
|
|
3308
|
+
const idx = this.retryQueue.indexOf(item);
|
|
3309
|
+
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3310
|
+
continue;
|
|
3311
|
+
}
|
|
3312
|
+
console.log(
|
|
3313
|
+
`Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
|
|
3314
|
+
);
|
|
3315
|
+
const result = await this.submitRecord(item.params);
|
|
3316
|
+
if (result.success) {
|
|
3317
|
+
const idx = this.retryQueue.indexOf(item);
|
|
3318
|
+
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
// ============================================================
|
|
3323
|
+
// CONVERSION HELPERS
|
|
3324
|
+
// ============================================================
|
|
3325
|
+
/**
|
|
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.21";
|
|
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
|
|
3159
3816
|
*/
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
const idx = this.retryQueue.indexOf(item);
|
|
3174
|
-
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3175
|
-
continue;
|
|
3176
|
-
}
|
|
3177
|
-
console.log(
|
|
3178
|
-
`Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
|
|
3179
|
-
);
|
|
3180
|
-
const result = await this.submitRecord(item.params);
|
|
3181
|
-
if (result.success) {
|
|
3182
|
-
const idx = this.retryQueue.indexOf(item);
|
|
3183
|
-
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3184
|
-
}
|
|
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;
|
|
3185
3830
|
}
|
|
3186
3831
|
}
|
|
3187
|
-
// ============================================================
|
|
3188
|
-
// CONVERSION HELPERS
|
|
3189
|
-
// ============================================================
|
|
3190
3832
|
/**
|
|
3191
|
-
*
|
|
3192
|
-
* notionalUSD = px * sz * 1e6
|
|
3833
|
+
* Send a status heartbeat
|
|
3193
3834
|
*/
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3835
|
+
sendHeartbeat(status) {
|
|
3836
|
+
if (!this.authenticated) return;
|
|
3837
|
+
this.send({
|
|
3838
|
+
type: "heartbeat",
|
|
3839
|
+
agentId: this.config.agentId,
|
|
3840
|
+
status
|
|
3841
|
+
});
|
|
3198
3842
|
}
|
|
3199
3843
|
/**
|
|
3200
|
-
*
|
|
3201
|
-
* feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
|
|
3844
|
+
* Send a status update (outside of regular heartbeat)
|
|
3202
3845
|
*/
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
// src/perp/onboarding.ts
|
|
3211
|
-
var PerpOnboarding = class {
|
|
3212
|
-
client;
|
|
3213
|
-
signer;
|
|
3214
|
-
config;
|
|
3215
|
-
constructor(client, signer, config) {
|
|
3216
|
-
this.client = client;
|
|
3217
|
-
this.signer = signer;
|
|
3218
|
-
this.config = config;
|
|
3846
|
+
sendStatusUpdate(status) {
|
|
3847
|
+
if (!this.authenticated) return;
|
|
3848
|
+
this.send({
|
|
3849
|
+
type: "status_update",
|
|
3850
|
+
agentId: this.config.agentId,
|
|
3851
|
+
status
|
|
3852
|
+
});
|
|
3219
3853
|
}
|
|
3220
|
-
// ============================================================
|
|
3221
|
-
// BUILDER FEE
|
|
3222
|
-
// ============================================================
|
|
3223
3854
|
/**
|
|
3224
|
-
*
|
|
3225
|
-
* Builder fee must be approved before orders can include builder fees.
|
|
3855
|
+
* Send a message to the command center
|
|
3226
3856
|
*/
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
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
|
+
});
|
|
3237
3868
|
}
|
|
3238
3869
|
/**
|
|
3239
|
-
*
|
|
3240
|
-
* This is a one-time approval per builder address.
|
|
3870
|
+
* Send a command execution result
|
|
3241
3871
|
*/
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
const { signature } = await this.signer.signApproval(action);
|
|
3252
|
-
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
3253
|
-
method: "POST",
|
|
3254
|
-
headers: { "Content-Type": "application/json" },
|
|
3255
|
-
body: JSON.stringify({
|
|
3256
|
-
action,
|
|
3257
|
-
signature: {
|
|
3258
|
-
r: signature.slice(0, 66),
|
|
3259
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
3260
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
3261
|
-
},
|
|
3262
|
-
nonce: action.nonce,
|
|
3263
|
-
vaultAddress: null
|
|
3264
|
-
})
|
|
3265
|
-
});
|
|
3266
|
-
if (!resp.ok) {
|
|
3267
|
-
const text = await resp.text();
|
|
3268
|
-
console.error(`Builder fee approval failed: ${resp.status} ${text}`);
|
|
3269
|
-
return false;
|
|
3270
|
-
}
|
|
3271
|
-
console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
|
|
3272
|
-
return true;
|
|
3273
|
-
} catch (error) {
|
|
3274
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3275
|
-
console.error(`Builder fee approval failed: ${message}`);
|
|
3276
|
-
return false;
|
|
3277
|
-
}
|
|
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
|
+
});
|
|
3278
3881
|
}
|
|
3279
|
-
// ============================================================
|
|
3280
|
-
// BALANCE & REQUIREMENTS
|
|
3281
|
-
// ============================================================
|
|
3282
3882
|
/**
|
|
3283
|
-
*
|
|
3284
|
-
* Returns the account equity in USD.
|
|
3883
|
+
* Start the heartbeat timer
|
|
3285
3884
|
*/
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
return { hasBalance: false, equity: 0 };
|
|
3295
|
-
}
|
|
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();
|
|
3891
|
+
}
|
|
3892
|
+
}, interval);
|
|
3296
3893
|
}
|
|
3297
3894
|
/**
|
|
3298
|
-
*
|
|
3299
|
-
* Perps require risk universe >= 2 (Derivatives or higher).
|
|
3895
|
+
* Stop the heartbeat timer
|
|
3300
3896
|
*/
|
|
3301
|
-
|
|
3302
|
-
if (
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
message: `Risk universe ${riskUniverse} allows perp trading`
|
|
3306
|
-
};
|
|
3897
|
+
stopHeartbeat() {
|
|
3898
|
+
if (this.heartbeatTimer) {
|
|
3899
|
+
clearInterval(this.heartbeatTimer);
|
|
3900
|
+
this.heartbeatTimer = null;
|
|
3307
3901
|
}
|
|
3308
|
-
return {
|
|
3309
|
-
allowed: false,
|
|
3310
|
-
message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
|
|
3311
|
-
};
|
|
3312
3902
|
}
|
|
3313
|
-
// ============================================================
|
|
3314
|
-
// FULL ONBOARDING CHECK
|
|
3315
|
-
// ============================================================
|
|
3316
3903
|
/**
|
|
3317
|
-
*
|
|
3318
|
-
* Does NOT auto-approve — caller must explicitly approve after review.
|
|
3904
|
+
* Schedule a reconnection with exponential backoff
|
|
3319
3905
|
*/
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
};
|
|
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;
|
|
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);
|
|
3335
3921
|
}
|
|
3336
3922
|
/**
|
|
3337
|
-
*
|
|
3338
|
-
* Returns the final status after all actions.
|
|
3923
|
+
* Send a JSON message to the WebSocket
|
|
3339
3924
|
*/
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
|
|
3344
|
-
return status;
|
|
3345
|
-
}
|
|
3346
|
-
if (!status.hasBalance) {
|
|
3347
|
-
console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
|
|
3348
|
-
return status;
|
|
3349
|
-
}
|
|
3350
|
-
if (!status.builderFeeApproved) {
|
|
3351
|
-
console.log("Approving builder fee...");
|
|
3352
|
-
const approved = await this.approveBuilderFee();
|
|
3353
|
-
if (approved) {
|
|
3354
|
-
status = { ...status, builderFeeApproved: true, ready: true };
|
|
3355
|
-
}
|
|
3925
|
+
send(data) {
|
|
3926
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
3927
|
+
this.ws.send(JSON.stringify(data));
|
|
3356
3928
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Check if connected and authenticated
|
|
3932
|
+
*/
|
|
3933
|
+
get isConnected() {
|
|
3934
|
+
return this.authenticated && this.ws?.readyState === import_ws2.default.OPEN;
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Disconnect and stop reconnecting
|
|
3938
|
+
*/
|
|
3939
|
+
disconnect() {
|
|
3940
|
+
this.stopped = true;
|
|
3941
|
+
this.stopHeartbeat();
|
|
3942
|
+
if (this.reconnectTimer) {
|
|
3943
|
+
clearTimeout(this.reconnectTimer);
|
|
3944
|
+
this.reconnectTimer = null;
|
|
3359
3945
|
}
|
|
3360
|
-
|
|
3946
|
+
if (this.ws) {
|
|
3947
|
+
this.ws.close(1e3, "Agent shutting down");
|
|
3948
|
+
this.ws = null;
|
|
3949
|
+
}
|
|
3950
|
+
this.authenticated = false;
|
|
3951
|
+
console.log("Relay: Disconnected");
|
|
3361
3952
|
}
|
|
3362
3953
|
};
|
|
3363
3954
|
|
|
3364
|
-
// src/
|
|
3365
|
-
var
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
"event MessageSent(bytes message)"
|
|
3380
|
-
]);
|
|
3381
|
-
var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
|
|
3382
|
-
"function deposit(uint256 amount, uint32 destinationDex) external"
|
|
3383
|
-
]);
|
|
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
|
+
}
|
|
3384
3970
|
|
|
3385
3971
|
// src/runtime.ts
|
|
3386
3972
|
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
@@ -3402,9 +3988,12 @@ var AgentRuntime = class {
|
|
|
3402
3988
|
lastCycleAt = 0;
|
|
3403
3989
|
lastPortfolioValue = 0;
|
|
3404
3990
|
lastEthBalance = "0";
|
|
3991
|
+
lastPrices = {};
|
|
3405
3992
|
processAlive = true;
|
|
3406
3993
|
riskUniverse = 0;
|
|
3407
3994
|
allowedTokens = /* @__PURE__ */ new Set();
|
|
3995
|
+
strategyContext;
|
|
3996
|
+
positionTracker;
|
|
3408
3997
|
// Perp trading components (null if perp not enabled)
|
|
3409
3998
|
perpClient = null;
|
|
3410
3999
|
perpSigner = null;
|
|
@@ -3429,7 +4018,7 @@ var AgentRuntime = class {
|
|
|
3429
4018
|
*/
|
|
3430
4019
|
async initialize() {
|
|
3431
4020
|
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
3432
|
-
this.client = new
|
|
4021
|
+
this.client = new import_sdk.ExagentClient({
|
|
3433
4022
|
privateKey: this.config.privateKey,
|
|
3434
4023
|
network: this.config.network
|
|
3435
4024
|
});
|
|
@@ -3447,9 +4036,20 @@ var AgentRuntime = class {
|
|
|
3447
4036
|
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
3448
4037
|
await this.syncConfigHash();
|
|
3449
4038
|
this.strategy = await loadStrategy();
|
|
4039
|
+
const store = new FileStore();
|
|
4040
|
+
this.strategyContext = {
|
|
4041
|
+
store,
|
|
4042
|
+
agentId: Number(this.config.agentId),
|
|
4043
|
+
walletAddress: this.client.address
|
|
4044
|
+
};
|
|
4045
|
+
this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
|
|
3450
4046
|
this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
|
|
3451
4047
|
this.riskManager = new RiskManager(this.config.trading);
|
|
3452
4048
|
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
4049
|
+
const savedRisk = this.positionTracker.getRiskState();
|
|
4050
|
+
if (savedRisk.lastResetDate) {
|
|
4051
|
+
this.riskManager.restoreState(savedRisk);
|
|
4052
|
+
}
|
|
3453
4053
|
await this.initializeVaultManager();
|
|
3454
4054
|
await this.initializePerp();
|
|
3455
4055
|
await this.initializeRelay();
|
|
@@ -3635,7 +4235,7 @@ var AgentRuntime = class {
|
|
|
3635
4235
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
3636
4236
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
3637
4237
|
const nonce = await this.client.registry.getNonce(address);
|
|
3638
|
-
const linkMessage =
|
|
4238
|
+
const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
|
|
3639
4239
|
address,
|
|
3640
4240
|
agentId,
|
|
3641
4241
|
nonce
|
|
@@ -3726,18 +4326,17 @@ var AgentRuntime = class {
|
|
|
3726
4326
|
async syncConfigHash() {
|
|
3727
4327
|
const agentId = BigInt(this.config.agentId);
|
|
3728
4328
|
const llmMeta = this.llm.getMetadata();
|
|
3729
|
-
this.configHash =
|
|
4329
|
+
this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
3730
4330
|
console.log(`Config hash: ${this.configHash}`);
|
|
3731
4331
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
3732
4332
|
if (onChainHash !== this.configHash) {
|
|
3733
4333
|
console.log("Config changed, updating on-chain...");
|
|
3734
4334
|
try {
|
|
3735
4335
|
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
3736
|
-
|
|
3737
|
-
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
4336
|
+
console.log(`Config updated on-chain`);
|
|
3738
4337
|
} catch (error) {
|
|
3739
4338
|
const message = error instanceof Error ? error.message : String(error);
|
|
3740
|
-
if (message.includes("insufficient funds") || message.includes("
|
|
4339
|
+
if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
3741
4340
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
3742
4341
|
const chain = import_chains4.base;
|
|
3743
4342
|
const publicClientInstance = (0, import_viem6.createPublicClient)({
|
|
@@ -3765,19 +4364,18 @@ var AgentRuntime = class {
|
|
|
3765
4364
|
console.log(" ETH detected! Retrying config update...");
|
|
3766
4365
|
console.log("");
|
|
3767
4366
|
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
3768
|
-
|
|
3769
|
-
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
4367
|
+
console.log(`Config updated on-chain`);
|
|
3770
4368
|
return;
|
|
3771
4369
|
}
|
|
3772
4370
|
process.stdout.write(".");
|
|
3773
4371
|
}
|
|
3774
4372
|
} else {
|
|
3775
|
-
|
|
4373
|
+
console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
|
|
4374
|
+
this.configHash = onChainHash;
|
|
3776
4375
|
}
|
|
3777
4376
|
}
|
|
3778
4377
|
} else {
|
|
3779
|
-
|
|
3780
|
-
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
4378
|
+
console.log("Config hash matches on-chain");
|
|
3781
4379
|
}
|
|
3782
4380
|
}
|
|
3783
4381
|
/**
|
|
@@ -3911,6 +4509,10 @@ var AgentRuntime = class {
|
|
|
3911
4509
|
}
|
|
3912
4510
|
if (updated) {
|
|
3913
4511
|
this.riskManager = new RiskManager(this.config.trading);
|
|
4512
|
+
const savedRiskState = this.positionTracker.getRiskState();
|
|
4513
|
+
if (savedRiskState.lastResetDate) {
|
|
4514
|
+
this.riskManager.restoreState(savedRiskState);
|
|
4515
|
+
}
|
|
3914
4516
|
console.log("Risk params updated via command center");
|
|
3915
4517
|
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
3916
4518
|
this.relay?.sendMessage(
|
|
@@ -4114,6 +4716,7 @@ var AgentRuntime = class {
|
|
|
4114
4716
|
mode: this.mode,
|
|
4115
4717
|
agentId: String(this.config.agentId),
|
|
4116
4718
|
wallet: this.client?.address,
|
|
4719
|
+
sdkVersion: AGENT_VERSION,
|
|
4117
4720
|
cycleCount: this.cycleCount,
|
|
4118
4721
|
lastCycleAt: this.lastCycleAt,
|
|
4119
4722
|
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
@@ -4142,7 +4745,8 @@ var AgentRuntime = class {
|
|
|
4142
4745
|
openPositions: 0,
|
|
4143
4746
|
effectiveLeverage: 0,
|
|
4144
4747
|
pendingRecords: this.perpRecorder?.pendingRetries ?? 0
|
|
4145
|
-
} : void 0
|
|
4748
|
+
} : void 0,
|
|
4749
|
+
positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0
|
|
4146
4750
|
};
|
|
4147
4751
|
if (this.perpConnected && this.perpPositions && status.perp) {
|
|
4148
4752
|
this.perpPositions.getAccountSummary().then((account) => {
|
|
@@ -4173,17 +4777,22 @@ var AgentRuntime = class {
|
|
|
4173
4777
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
4174
4778
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
4175
4779
|
this.lastPortfolioValue = marketData.portfolioValue;
|
|
4780
|
+
this.lastPrices = marketData.prices;
|
|
4176
4781
|
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4177
4782
|
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
4783
|
+
this.positionTracker.syncBalances(marketData.balances, marketData.prices);
|
|
4178
4784
|
const fundsOk = this.checkFundsLow(marketData);
|
|
4179
4785
|
if (!fundsOk) {
|
|
4180
4786
|
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
4181
4787
|
this.sendRelayStatus();
|
|
4182
4788
|
return;
|
|
4183
4789
|
}
|
|
4790
|
+
this.strategyContext.positions = this.positionTracker.getPositions();
|
|
4791
|
+
this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
|
|
4792
|
+
this.strategyContext.positionTracker = this.positionTracker;
|
|
4184
4793
|
let signals;
|
|
4185
4794
|
try {
|
|
4186
|
-
signals = await this.strategy(marketData, this.llm, this.config);
|
|
4795
|
+
signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
|
|
4187
4796
|
} catch (error) {
|
|
4188
4797
|
const message = error instanceof Error ? error.message : String(error);
|
|
4189
4798
|
console.error("LLM/strategy error:", message);
|
|
@@ -4246,13 +4855,30 @@ var AgentRuntime = class {
|
|
|
4246
4855
|
);
|
|
4247
4856
|
}
|
|
4248
4857
|
}
|
|
4858
|
+
for (const result of results) {
|
|
4859
|
+
const tokenIn = result.signal.tokenIn.toLowerCase();
|
|
4860
|
+
const tokenOut = result.signal.tokenOut.toLowerCase();
|
|
4861
|
+
this.positionTracker.recordTrade({
|
|
4862
|
+
action: result.signal.action,
|
|
4863
|
+
tokenIn,
|
|
4864
|
+
tokenOut,
|
|
4865
|
+
amountIn: result.signal.amountIn,
|
|
4866
|
+
priceIn: marketData.prices[tokenIn] || 0,
|
|
4867
|
+
priceOut: marketData.prices[tokenOut] || 0,
|
|
4868
|
+
txHash: result.txHash,
|
|
4869
|
+
reasoning: result.signal.reasoning,
|
|
4870
|
+
success: result.success
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4249
4873
|
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
4250
4874
|
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
4251
4875
|
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
4252
4876
|
this.riskManager.updatePnL(marketPnL);
|
|
4877
|
+
this.positionTracker.saveRiskState(this.riskManager.exportState());
|
|
4253
4878
|
if (marketPnL !== 0) {
|
|
4254
4879
|
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
4255
4880
|
}
|
|
4881
|
+
this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
|
|
4256
4882
|
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
4257
4883
|
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4258
4884
|
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
@@ -4527,192 +5153,6 @@ var AgentRuntime = class {
|
|
|
4527
5153
|
|
|
4528
5154
|
// src/cli.ts
|
|
4529
5155
|
var import_accounts6 = require("viem/accounts");
|
|
4530
|
-
|
|
4531
|
-
// src/secure-env.ts
|
|
4532
|
-
var crypto = __toESM(require("crypto"));
|
|
4533
|
-
var fs = __toESM(require("fs"));
|
|
4534
|
-
var path = __toESM(require("path"));
|
|
4535
|
-
var ALGORITHM = "aes-256-gcm";
|
|
4536
|
-
var PBKDF2_ITERATIONS = 1e5;
|
|
4537
|
-
var SALT_LENGTH = 32;
|
|
4538
|
-
var IV_LENGTH = 16;
|
|
4539
|
-
var KEY_LENGTH = 32;
|
|
4540
|
-
var SENSITIVE_PATTERNS = [
|
|
4541
|
-
/PRIVATE_KEY$/i,
|
|
4542
|
-
/_API_KEY$/i,
|
|
4543
|
-
/API_KEY$/i,
|
|
4544
|
-
/_SECRET$/i,
|
|
4545
|
-
/^OPENAI_API_KEY$/i,
|
|
4546
|
-
/^ANTHROPIC_API_KEY$/i,
|
|
4547
|
-
/^GOOGLE_AI_API_KEY$/i,
|
|
4548
|
-
/^DEEPSEEK_API_KEY$/i,
|
|
4549
|
-
/^MISTRAL_API_KEY$/i,
|
|
4550
|
-
/^GROQ_API_KEY$/i,
|
|
4551
|
-
/^TOGETHER_API_KEY$/i
|
|
4552
|
-
];
|
|
4553
|
-
function isSensitiveKey(key) {
|
|
4554
|
-
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
4555
|
-
}
|
|
4556
|
-
function deriveKey(passphrase, salt) {
|
|
4557
|
-
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
4558
|
-
}
|
|
4559
|
-
function encryptValue(value, key) {
|
|
4560
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
4561
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
4562
|
-
let encrypted = cipher.update(value, "utf8", "hex");
|
|
4563
|
-
encrypted += cipher.final("hex");
|
|
4564
|
-
const tag = cipher.getAuthTag();
|
|
4565
|
-
return {
|
|
4566
|
-
iv: iv.toString("hex"),
|
|
4567
|
-
encrypted,
|
|
4568
|
-
tag: tag.toString("hex")
|
|
4569
|
-
};
|
|
4570
|
-
}
|
|
4571
|
-
function decryptValue(encrypted, key, iv, tag) {
|
|
4572
|
-
const decipher = crypto.createDecipheriv(
|
|
4573
|
-
ALGORITHM,
|
|
4574
|
-
key,
|
|
4575
|
-
Buffer.from(iv, "hex")
|
|
4576
|
-
);
|
|
4577
|
-
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
4578
|
-
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
4579
|
-
decrypted += decipher.final("utf8");
|
|
4580
|
-
return decrypted;
|
|
4581
|
-
}
|
|
4582
|
-
function parseEnvFile(content) {
|
|
4583
|
-
const entries = [];
|
|
4584
|
-
for (const line of content.split("\n")) {
|
|
4585
|
-
const trimmed = line.trim();
|
|
4586
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
4587
|
-
const eqIndex = trimmed.indexOf("=");
|
|
4588
|
-
if (eqIndex === -1) continue;
|
|
4589
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
4590
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
4591
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4592
|
-
value = value.slice(1, -1);
|
|
4593
|
-
}
|
|
4594
|
-
if (key && value) {
|
|
4595
|
-
entries.push({ key, value });
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
return entries;
|
|
4599
|
-
}
|
|
4600
|
-
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
4601
|
-
if (!fs.existsSync(envPath)) {
|
|
4602
|
-
throw new Error(`File not found: ${envPath}`);
|
|
4603
|
-
}
|
|
4604
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
4605
|
-
const entries = parseEnvFile(content);
|
|
4606
|
-
if (entries.length === 0) {
|
|
4607
|
-
throw new Error("No environment variables found in file");
|
|
4608
|
-
}
|
|
4609
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
4610
|
-
const key = deriveKey(passphrase, salt);
|
|
4611
|
-
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
4612
|
-
if (isSensitiveKey(envKey)) {
|
|
4613
|
-
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
4614
|
-
return {
|
|
4615
|
-
key: envKey,
|
|
4616
|
-
value: encrypted,
|
|
4617
|
-
encrypted: true,
|
|
4618
|
-
iv,
|
|
4619
|
-
tag
|
|
4620
|
-
};
|
|
4621
|
-
}
|
|
4622
|
-
return {
|
|
4623
|
-
key: envKey,
|
|
4624
|
-
value,
|
|
4625
|
-
encrypted: false
|
|
4626
|
-
};
|
|
4627
|
-
});
|
|
4628
|
-
const encryptedEnv = {
|
|
4629
|
-
version: 1,
|
|
4630
|
-
salt: salt.toString("hex"),
|
|
4631
|
-
entries: encryptedEntries
|
|
4632
|
-
};
|
|
4633
|
-
const encPath = envPath + ".enc";
|
|
4634
|
-
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
4635
|
-
if (deleteOriginal) {
|
|
4636
|
-
fs.unlinkSync(envPath);
|
|
4637
|
-
}
|
|
4638
|
-
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
4639
|
-
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
4640
|
-
console.log(
|
|
4641
|
-
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
4642
|
-
);
|
|
4643
|
-
return encPath;
|
|
4644
|
-
}
|
|
4645
|
-
function decryptEnvFile(encPath, passphrase) {
|
|
4646
|
-
if (!fs.existsSync(encPath)) {
|
|
4647
|
-
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
4648
|
-
}
|
|
4649
|
-
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
4650
|
-
if (content.version !== 1) {
|
|
4651
|
-
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
4652
|
-
}
|
|
4653
|
-
const salt = Buffer.from(content.salt, "hex");
|
|
4654
|
-
const key = deriveKey(passphrase, salt);
|
|
4655
|
-
const result = {};
|
|
4656
|
-
for (const entry of content.entries) {
|
|
4657
|
-
if (entry.encrypted) {
|
|
4658
|
-
if (!entry.iv || !entry.tag) {
|
|
4659
|
-
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
4660
|
-
}
|
|
4661
|
-
try {
|
|
4662
|
-
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
4663
|
-
} catch {
|
|
4664
|
-
throw new Error(
|
|
4665
|
-
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
4666
|
-
);
|
|
4667
|
-
}
|
|
4668
|
-
} else {
|
|
4669
|
-
result[entry.key] = entry.value;
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
return result;
|
|
4673
|
-
}
|
|
4674
|
-
function loadSecureEnv(basePath, passphrase) {
|
|
4675
|
-
const encPath = path.join(basePath, ".env.enc");
|
|
4676
|
-
const envPath = path.join(basePath, ".env");
|
|
4677
|
-
if (fs.existsSync(encPath)) {
|
|
4678
|
-
if (!passphrase) {
|
|
4679
|
-
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
4680
|
-
}
|
|
4681
|
-
if (!passphrase) {
|
|
4682
|
-
console.warn("");
|
|
4683
|
-
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
4684
|
-
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
4685
|
-
console.warn(" pass --passphrase when running the agent.");
|
|
4686
|
-
console.warn(" Falling back to plaintext .env file.");
|
|
4687
|
-
console.warn("");
|
|
4688
|
-
} else {
|
|
4689
|
-
const vars = decryptEnvFile(encPath, passphrase);
|
|
4690
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
4691
|
-
process.env[key] = value;
|
|
4692
|
-
}
|
|
4693
|
-
return true;
|
|
4694
|
-
}
|
|
4695
|
-
}
|
|
4696
|
-
if (fs.existsSync(envPath)) {
|
|
4697
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
4698
|
-
const entries = parseEnvFile(content);
|
|
4699
|
-
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
4700
|
-
if (sensitiveKeys.length > 0) {
|
|
4701
|
-
console.warn("");
|
|
4702
|
-
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
4703
|
-
for (const key of sensitiveKeys) {
|
|
4704
|
-
console.warn(` - ${key}`);
|
|
4705
|
-
}
|
|
4706
|
-
console.warn("");
|
|
4707
|
-
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
4708
|
-
console.warn("");
|
|
4709
|
-
}
|
|
4710
|
-
return false;
|
|
4711
|
-
}
|
|
4712
|
-
return false;
|
|
4713
|
-
}
|
|
4714
|
-
|
|
4715
|
-
// src/cli.ts
|
|
4716
5156
|
(0, import_dotenv2.config)();
|
|
4717
5157
|
var program = new import_commander.Command();
|
|
4718
5158
|
program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");
|