@exagent/agent 0.1.20 → 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/cli.js +1421 -1098
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +180 -4
- package/dist/index.d.ts +180 -4
- package/dist/index.js +806 -479
- package/dist/index.mjs +5 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -31,105 +31,641 @@ var fs2 = __toESM(require("fs"));
|
|
|
31
31
|
var path2 = __toESM(require("path"));
|
|
32
32
|
|
|
33
33
|
// src/runtime.ts
|
|
34
|
-
var
|
|
34
|
+
var import_sdk = require("@exagent/sdk");
|
|
35
35
|
var import_viem6 = require("viem");
|
|
36
36
|
var import_chains4 = require("viem/chains");
|
|
37
37
|
var import_accounts5 = require("viem/accounts");
|
|
38
38
|
|
|
39
|
-
// src/
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
// src/trading/market.ts
|
|
40
|
+
var import_viem = require("viem");
|
|
41
|
+
var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
42
|
+
var TOKEN_DECIMALS = {
|
|
43
|
+
// Base Mainnet — Core tokens
|
|
44
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
45
|
+
// USDC
|
|
46
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
47
|
+
// USDbC
|
|
48
|
+
"0x4200000000000000000000000000000000000006": 18,
|
|
49
|
+
// WETH
|
|
50
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
51
|
+
// DAI
|
|
52
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
53
|
+
// cbETH
|
|
54
|
+
[NATIVE_ETH.toLowerCase()]: 18,
|
|
55
|
+
// Native ETH
|
|
56
|
+
// Base Mainnet — Established tokens
|
|
57
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
58
|
+
// AERO (Aerodrome)
|
|
59
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
60
|
+
// BRETT
|
|
61
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
62
|
+
// DEGEN
|
|
63
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
64
|
+
// VIRTUAL
|
|
65
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
66
|
+
// TOSHI
|
|
67
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
68
|
+
// cbBTC
|
|
69
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
70
|
+
// ezETH (Renzo)
|
|
71
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
72
|
+
// wstETH (Lido)
|
|
73
|
+
};
|
|
74
|
+
function getTokenDecimals(address) {
|
|
75
|
+
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
76
|
+
if (decimals === void 0) {
|
|
77
|
+
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
78
|
+
return 18;
|
|
47
79
|
}
|
|
48
|
-
|
|
80
|
+
return decimals;
|
|
81
|
+
}
|
|
82
|
+
var TOKEN_TO_COINGECKO = {
|
|
83
|
+
// Core
|
|
84
|
+
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
85
|
+
// WETH
|
|
86
|
+
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
87
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
88
|
+
// USDC
|
|
89
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
90
|
+
// USDbC
|
|
91
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
92
|
+
// cbETH
|
|
93
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
94
|
+
// DAI
|
|
95
|
+
// Established
|
|
96
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
97
|
+
// AERO
|
|
98
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
99
|
+
// BRETT
|
|
100
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
101
|
+
// DEGEN
|
|
102
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
103
|
+
// VIRTUAL
|
|
104
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
105
|
+
// TOSHI
|
|
106
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
107
|
+
// cbBTC
|
|
108
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
109
|
+
// ezETH
|
|
110
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
111
|
+
// wstETH
|
|
112
|
+
};
|
|
113
|
+
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
114
|
+
var PRICE_STALENESS_MS = 6e4;
|
|
115
|
+
var MarketDataService = class {
|
|
116
|
+
rpcUrl;
|
|
117
|
+
client;
|
|
118
|
+
/** Cached prices from last fetch */
|
|
119
|
+
cachedPrices = {};
|
|
120
|
+
/** Timestamp of last successful price fetch */
|
|
121
|
+
lastPriceFetchAt = 0;
|
|
122
|
+
constructor(rpcUrl) {
|
|
123
|
+
this.rpcUrl = rpcUrl;
|
|
124
|
+
this.client = (0, import_viem.createPublicClient)({
|
|
125
|
+
transport: (0, import_viem.http)(rpcUrl)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Cached volume data */
|
|
129
|
+
cachedVolume24h = {};
|
|
130
|
+
/** Cached price change data */
|
|
131
|
+
cachedPriceChange24h = {};
|
|
132
|
+
/**
|
|
133
|
+
* Fetch current market data for the agent
|
|
134
|
+
*/
|
|
135
|
+
async fetchMarketData(walletAddress, tokenAddresses) {
|
|
136
|
+
const prices = await this.fetchPrices(tokenAddresses);
|
|
137
|
+
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
138
|
+
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
139
|
+
let gasPrice;
|
|
140
|
+
try {
|
|
141
|
+
gasPrice = await this.client.getGasPrice();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
49
144
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
prices,
|
|
147
|
+
balances,
|
|
148
|
+
portfolioValue,
|
|
149
|
+
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
150
|
+
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
151
|
+
gasPrice,
|
|
152
|
+
network: {
|
|
153
|
+
chainId: this.client.chain?.id ?? 8453
|
|
154
|
+
}
|
|
53
155
|
};
|
|
54
156
|
}
|
|
55
157
|
/**
|
|
56
|
-
*
|
|
158
|
+
* Check if cached prices are still fresh
|
|
57
159
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return `Local (${this.config.model || "ollama"})`;
|
|
61
|
-
}
|
|
62
|
-
return this.config.model || this.config.provider;
|
|
160
|
+
get pricesAreFresh() {
|
|
161
|
+
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
63
162
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Fetch token prices from CoinGecko free API
|
|
165
|
+
* Returns cached prices if still fresh (<60s old)
|
|
166
|
+
*/
|
|
167
|
+
async fetchPrices(tokenAddresses) {
|
|
168
|
+
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
169
|
+
const prices2 = { ...this.cachedPrices };
|
|
170
|
+
for (const addr of tokenAddresses) {
|
|
171
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
172
|
+
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
173
|
+
prices2[addr.toLowerCase()] = 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return prices2;
|
|
73
177
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const response = await this.client.chat.completions.create({
|
|
82
|
-
model: this.config.model || "gpt-4.1",
|
|
83
|
-
messages: messages.map((m) => ({
|
|
84
|
-
role: m.role,
|
|
85
|
-
content: m.content
|
|
86
|
-
})),
|
|
87
|
-
temperature: this.config.temperature,
|
|
88
|
-
max_tokens: this.config.maxTokens
|
|
89
|
-
});
|
|
90
|
-
const choice = response.choices[0];
|
|
91
|
-
if (!choice || !choice.message) {
|
|
92
|
-
throw new Error("No response from OpenAI");
|
|
178
|
+
const prices = {};
|
|
179
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
180
|
+
for (const addr of tokenAddresses) {
|
|
181
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
182
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
183
|
+
idsToFetch.add(cgId);
|
|
93
184
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
185
|
+
}
|
|
186
|
+
idsToFetch.add("ethereum");
|
|
187
|
+
if (idsToFetch.size > 0) {
|
|
188
|
+
try {
|
|
189
|
+
const ids = Array.from(idsToFetch).join(",");
|
|
190
|
+
const response = await fetch(
|
|
191
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
192
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
193
|
+
);
|
|
194
|
+
if (response.ok) {
|
|
195
|
+
const data = await response.json();
|
|
196
|
+
for (const [cgId, priceData] of Object.entries(data)) {
|
|
197
|
+
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
198
|
+
if (id === cgId) {
|
|
199
|
+
const key = addr.toLowerCase();
|
|
200
|
+
prices[key] = priceData.usd;
|
|
201
|
+
if (priceData.usd_24h_vol !== void 0) {
|
|
202
|
+
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
203
|
+
}
|
|
204
|
+
if (priceData.usd_24h_change !== void 0) {
|
|
205
|
+
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.lastPriceFetchAt = Date.now();
|
|
211
|
+
} else {
|
|
212
|
+
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
105
216
|
}
|
|
106
|
-
throw error;
|
|
107
217
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
apiKey;
|
|
114
|
-
baseUrl;
|
|
115
|
-
constructor(config) {
|
|
116
|
-
super(config);
|
|
117
|
-
if (!config.apiKey) {
|
|
118
|
-
throw new Error("Anthropic API key required");
|
|
218
|
+
for (const addr of tokenAddresses) {
|
|
219
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
220
|
+
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
221
|
+
prices[addr.toLowerCase()] = 1;
|
|
222
|
+
}
|
|
119
223
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
const missingAddrs = tokenAddresses.filter(
|
|
225
|
+
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
226
|
+
);
|
|
227
|
+
if (missingAddrs.length > 0) {
|
|
228
|
+
try {
|
|
229
|
+
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
230
|
+
const llamaResponse = await fetch(
|
|
231
|
+
`https://coins.llama.fi/prices/current/${coins}`,
|
|
232
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
233
|
+
);
|
|
234
|
+
if (llamaResponse.ok) {
|
|
235
|
+
const llamaData = await llamaResponse.json();
|
|
236
|
+
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
237
|
+
const addr = key.replace("base:", "").toLowerCase();
|
|
238
|
+
if (data.price && data.confidence > 0.5) {
|
|
239
|
+
prices[addr] = data.price;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (Object.keys(prices).length > 0) {
|
|
248
|
+
this.cachedPrices = prices;
|
|
249
|
+
}
|
|
250
|
+
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
251
|
+
console.warn("Using cached prices (last successful fetch was stale)");
|
|
252
|
+
return { ...this.cachedPrices };
|
|
253
|
+
}
|
|
254
|
+
for (const addr of tokenAddresses) {
|
|
255
|
+
if (!prices[addr.toLowerCase()]) {
|
|
256
|
+
console.warn(`No price available for ${addr}, using 0`);
|
|
257
|
+
prices[addr.toLowerCase()] = 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return prices;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Fetch real on-chain balances: native ETH + ERC-20 tokens
|
|
264
|
+
*/
|
|
265
|
+
async fetchBalances(walletAddress, tokenAddresses) {
|
|
266
|
+
const balances = {};
|
|
267
|
+
const wallet = walletAddress;
|
|
268
|
+
try {
|
|
269
|
+
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
270
|
+
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
271
|
+
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
272
|
+
try {
|
|
273
|
+
const balance = await this.client.readContract({
|
|
274
|
+
address: tokenAddress,
|
|
275
|
+
abi: import_viem.erc20Abi,
|
|
276
|
+
functionName: "balanceOf",
|
|
277
|
+
args: [wallet]
|
|
278
|
+
});
|
|
279
|
+
return { address: tokenAddress.toLowerCase(), balance };
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const results = await Promise.all(erc20Promises);
|
|
285
|
+
for (const { address, balance } of results) {
|
|
286
|
+
balances[address] = balance;
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
290
|
+
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
291
|
+
for (const address of tokenAddresses) {
|
|
292
|
+
balances[address.toLowerCase()] = 0n;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return balances;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Calculate total portfolio value in USD
|
|
299
|
+
*/
|
|
300
|
+
calculatePortfolioValue(balances, prices) {
|
|
301
|
+
let total = 0;
|
|
302
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
303
|
+
const price = prices[address.toLowerCase()] || 0;
|
|
304
|
+
const decimals = getTokenDecimals(address);
|
|
305
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
306
|
+
total += amount * price;
|
|
307
|
+
}
|
|
308
|
+
return total;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/position-tracker.ts
|
|
313
|
+
var BASE_ASSETS = /* @__PURE__ */ new Set([
|
|
314
|
+
NATIVE_ETH.toLowerCase(),
|
|
315
|
+
// Native ETH
|
|
316
|
+
"0x4200000000000000000000000000000000000006",
|
|
317
|
+
// WETH
|
|
318
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
319
|
+
// USDC
|
|
320
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
|
|
321
|
+
// USDbC
|
|
322
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
|
|
323
|
+
// DAI
|
|
324
|
+
"0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
|
|
325
|
+
// USDT
|
|
326
|
+
"0x60a3e35cc302bfa44cb36dc100b2587cd09b9c83"
|
|
327
|
+
// EURC
|
|
328
|
+
]);
|
|
329
|
+
var TOKEN_SYMBOLS = {
|
|
330
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "AERO",
|
|
331
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "BRETT",
|
|
332
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "DEGEN",
|
|
333
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "VIRTUAL",
|
|
334
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "TOSHI",
|
|
335
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "cbBTC",
|
|
336
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "ezETH",
|
|
337
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wstETH",
|
|
338
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "cbETH",
|
|
339
|
+
"0x13403fb738c97cf7564f279288468c140aaed05c": "EXA"
|
|
340
|
+
};
|
|
341
|
+
var KEY_POSITIONS = "__positions";
|
|
342
|
+
var KEY_TRADE_HISTORY = "__trade_history";
|
|
343
|
+
var KEY_RISK_STATE = "__risk_state";
|
|
344
|
+
var PositionTracker = class {
|
|
345
|
+
store;
|
|
346
|
+
positions;
|
|
347
|
+
tradeHistory;
|
|
348
|
+
maxTradeHistory;
|
|
349
|
+
constructor(store, options) {
|
|
350
|
+
this.store = store;
|
|
351
|
+
this.maxTradeHistory = options?.maxTradeHistory ?? 50;
|
|
352
|
+
this.positions = store.get(KEY_POSITIONS) || {};
|
|
353
|
+
this.tradeHistory = store.get(KEY_TRADE_HISTORY) || [];
|
|
354
|
+
const posCount = Object.keys(this.positions).length;
|
|
355
|
+
if (posCount > 0 || this.tradeHistory.length > 0) {
|
|
356
|
+
console.log(`Position tracker loaded: ${posCount} positions, ${this.tradeHistory.length} trade records`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// ============================================================
|
|
360
|
+
// TRADE RECORDING (called by runtime after execution)
|
|
361
|
+
// ============================================================
|
|
362
|
+
/**
|
|
363
|
+
* Record a trade result. On buy: creates/updates position with cost-basis
|
|
364
|
+
* weighted average. On sell: calculates realized PnL and removes if fully sold.
|
|
365
|
+
*/
|
|
366
|
+
recordTrade(params) {
|
|
367
|
+
const { action, tokenIn, tokenOut, amountIn, priceIn, priceOut, txHash, reasoning, success } = params;
|
|
368
|
+
const decimalsIn = getTokenDecimals(tokenIn);
|
|
369
|
+
const amountInUnits = Number(amountIn) / Math.pow(10, decimalsIn);
|
|
370
|
+
const tradeValueUSD = amountInUnits * priceIn;
|
|
371
|
+
const record = {
|
|
372
|
+
timestamp: Date.now(),
|
|
373
|
+
action,
|
|
374
|
+
tokenIn: tokenIn.toLowerCase(),
|
|
375
|
+
tokenOut: tokenOut.toLowerCase(),
|
|
376
|
+
amountIn: amountIn.toString(),
|
|
377
|
+
priceUSD: tradeValueUSD,
|
|
378
|
+
txHash,
|
|
379
|
+
reasoning,
|
|
380
|
+
success
|
|
381
|
+
};
|
|
382
|
+
if (success) {
|
|
383
|
+
if (action === "buy") {
|
|
384
|
+
this.handleBuy(tokenOut.toLowerCase(), tradeValueUSD, priceOut, txHash);
|
|
385
|
+
} else if (action === "sell") {
|
|
386
|
+
const realizedPnL = this.handleSell(tokenIn.toLowerCase(), tradeValueUSD);
|
|
387
|
+
record.realizedPnL = realizedPnL;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.tradeHistory.unshift(record);
|
|
391
|
+
if (this.tradeHistory.length > this.maxTradeHistory) {
|
|
392
|
+
this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
|
|
393
|
+
}
|
|
394
|
+
this.persist();
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Handle a buy: create or update position with cost-basis weighted average.
|
|
398
|
+
*/
|
|
399
|
+
handleBuy(token, costUSD, priceUSD, txHash) {
|
|
400
|
+
if (BASE_ASSETS.has(token) || priceUSD <= 0 || costUSD <= 0) return;
|
|
401
|
+
const existing = this.positions[token];
|
|
402
|
+
const acquiredAmount = costUSD / priceUSD;
|
|
403
|
+
if (existing) {
|
|
404
|
+
const newTotalCost = existing.totalCostBasis + costUSD;
|
|
405
|
+
const newTotalAmount = existing.totalAmountAcquired + acquiredAmount;
|
|
406
|
+
existing.averageEntryPrice = newTotalAmount > 0 ? newTotalCost / newTotalAmount : priceUSD;
|
|
407
|
+
existing.totalCostBasis = newTotalCost;
|
|
408
|
+
existing.totalAmountAcquired = newTotalAmount;
|
|
409
|
+
existing.lastUpdateTimestamp = Date.now();
|
|
410
|
+
if (txHash) {
|
|
411
|
+
existing.txHashes.push(txHash);
|
|
412
|
+
if (existing.txHashes.length > 10) existing.txHashes.shift();
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
this.positions[token] = {
|
|
416
|
+
token,
|
|
417
|
+
symbol: TOKEN_SYMBOLS[token],
|
|
418
|
+
entryPrice: priceUSD,
|
|
419
|
+
averageEntryPrice: priceUSD,
|
|
420
|
+
totalCostBasis: costUSD,
|
|
421
|
+
totalAmountAcquired: acquiredAmount,
|
|
422
|
+
currentAmount: acquiredAmount,
|
|
423
|
+
entryTimestamp: Date.now(),
|
|
424
|
+
lastUpdateTimestamp: Date.now(),
|
|
425
|
+
txHashes: txHash ? [txHash] : []
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Handle a sell: calculate realized PnL and remove position if fully sold.
|
|
431
|
+
* Returns the realized PnL in USD.
|
|
432
|
+
*/
|
|
433
|
+
handleSell(token, saleValueUSD) {
|
|
434
|
+
if (BASE_ASSETS.has(token)) return 0;
|
|
435
|
+
const position = this.positions[token];
|
|
436
|
+
if (!position || position.averageEntryPrice <= 0) return 0;
|
|
437
|
+
const estimatedUnitsSold = position.currentAmount > 0 ? Math.min(position.currentAmount, saleValueUSD / position.averageEntryPrice) : saleValueUSD / position.averageEntryPrice;
|
|
438
|
+
const costBasisOfSold = estimatedUnitsSold * position.averageEntryPrice;
|
|
439
|
+
const realizedPnL = saleValueUSD - costBasisOfSold;
|
|
440
|
+
position.totalAmountAcquired = Math.max(0, position.totalAmountAcquired - estimatedUnitsSold);
|
|
441
|
+
position.totalCostBasis = Math.max(0, position.totalCostBasis - costBasisOfSold);
|
|
442
|
+
position.lastUpdateTimestamp = Date.now();
|
|
443
|
+
return realizedPnL;
|
|
444
|
+
}
|
|
445
|
+
// ============================================================
|
|
446
|
+
// BALANCE SYNC (called by runtime each cycle)
|
|
447
|
+
// ============================================================
|
|
448
|
+
/**
|
|
449
|
+
* Sync tracked positions with on-chain balances.
|
|
450
|
+
* Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
|
|
451
|
+
*/
|
|
452
|
+
syncBalances(balances, prices) {
|
|
453
|
+
let changed = false;
|
|
454
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
455
|
+
const token = address.toLowerCase();
|
|
456
|
+
if (BASE_ASSETS.has(token)) continue;
|
|
457
|
+
const decimals = getTokenDecimals(token);
|
|
458
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
459
|
+
if (amount > 0) {
|
|
460
|
+
if (this.positions[token]) {
|
|
461
|
+
if (this.positions[token].currentAmount !== amount) {
|
|
462
|
+
this.positions[token].currentAmount = amount;
|
|
463
|
+
this.positions[token].lastUpdateTimestamp = Date.now();
|
|
464
|
+
changed = true;
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
const price = prices[token] || 0;
|
|
468
|
+
this.positions[token] = {
|
|
469
|
+
token,
|
|
470
|
+
symbol: TOKEN_SYMBOLS[token],
|
|
471
|
+
entryPrice: price,
|
|
472
|
+
averageEntryPrice: price,
|
|
473
|
+
totalCostBasis: amount * price,
|
|
474
|
+
totalAmountAcquired: amount,
|
|
475
|
+
currentAmount: amount,
|
|
476
|
+
entryTimestamp: Date.now(),
|
|
477
|
+
lastUpdateTimestamp: Date.now(),
|
|
478
|
+
txHashes: []
|
|
479
|
+
};
|
|
480
|
+
if (price > 0) {
|
|
481
|
+
console.log(`Position tracker: detected new holding ${TOKEN_SYMBOLS[token] || token.slice(0, 10)} (${amount.toFixed(4)} units @ $${price.toFixed(4)})`);
|
|
482
|
+
}
|
|
483
|
+
changed = true;
|
|
484
|
+
}
|
|
485
|
+
} else if (this.positions[token]) {
|
|
486
|
+
delete this.positions[token];
|
|
487
|
+
changed = true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (changed) {
|
|
491
|
+
this.persist();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// ============================================================
|
|
495
|
+
// QUERY METHODS (for strategies)
|
|
496
|
+
// ============================================================
|
|
497
|
+
/** Get all tracked positions */
|
|
498
|
+
getPositions() {
|
|
499
|
+
return Object.values(this.positions);
|
|
500
|
+
}
|
|
501
|
+
/** Get a single position by token address */
|
|
502
|
+
getPosition(token) {
|
|
503
|
+
return this.positions[token.toLowerCase()];
|
|
504
|
+
}
|
|
505
|
+
/** Get trade history (newest first) */
|
|
506
|
+
getTradeHistory(limit) {
|
|
507
|
+
return limit ? this.tradeHistory.slice(0, limit) : [...this.tradeHistory];
|
|
508
|
+
}
|
|
509
|
+
/** Get unrealized PnL per position given current prices */
|
|
510
|
+
getUnrealizedPnL(prices) {
|
|
511
|
+
const pnl = {};
|
|
512
|
+
for (const pos of Object.values(this.positions)) {
|
|
513
|
+
const currentPrice = prices[pos.token] || 0;
|
|
514
|
+
if (currentPrice > 0 && pos.averageEntryPrice > 0 && pos.currentAmount > 0) {
|
|
515
|
+
const currentValue = pos.currentAmount * currentPrice;
|
|
516
|
+
const costBasis = pos.currentAmount * pos.averageEntryPrice;
|
|
517
|
+
pnl[pos.token] = currentValue - costBasis;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return pnl;
|
|
521
|
+
}
|
|
522
|
+
/** Get total unrealized PnL across all positions */
|
|
523
|
+
getTotalUnrealizedPnL(prices) {
|
|
524
|
+
const pnl = this.getUnrealizedPnL(prices);
|
|
525
|
+
return Object.values(pnl).reduce((sum, v) => sum + v, 0);
|
|
526
|
+
}
|
|
527
|
+
// ============================================================
|
|
528
|
+
// RISK STATE PERSISTENCE
|
|
529
|
+
// ============================================================
|
|
530
|
+
/** Load persisted risk state */
|
|
531
|
+
getRiskState() {
|
|
532
|
+
return this.store.get(KEY_RISK_STATE) || {
|
|
533
|
+
dailyPnL: 0,
|
|
534
|
+
dailyFees: 0,
|
|
535
|
+
lastResetDate: ""
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/** Save risk state to persistent store */
|
|
539
|
+
saveRiskState(state) {
|
|
540
|
+
this.store.set(KEY_RISK_STATE, state);
|
|
541
|
+
}
|
|
542
|
+
// ============================================================
|
|
543
|
+
// RELAY SUMMARY
|
|
544
|
+
// ============================================================
|
|
545
|
+
/** Get a compact summary for relay heartbeats */
|
|
546
|
+
getPositionSummary(prices) {
|
|
547
|
+
const unrealizedPnL = this.getUnrealizedPnL(prices);
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
const topPositions = Object.values(this.positions).map((pos) => ({
|
|
550
|
+
token: pos.token,
|
|
551
|
+
symbol: pos.symbol,
|
|
552
|
+
unrealizedPnL: unrealizedPnL[pos.token] || 0,
|
|
553
|
+
holdingDuration: now - pos.entryTimestamp
|
|
554
|
+
})).sort((a, b) => Math.abs(b.unrealizedPnL) - Math.abs(a.unrealizedPnL)).slice(0, 5);
|
|
555
|
+
const oneDayAgo = now - 24 * 60 * 60 * 1e3;
|
|
556
|
+
const recentTrades = this.tradeHistory.filter((t) => t.timestamp > oneDayAgo).length;
|
|
557
|
+
const totalRealizedPnL = this.tradeHistory.filter((t) => t.realizedPnL !== void 0).reduce((sum, t) => sum + (t.realizedPnL || 0), 0);
|
|
558
|
+
return {
|
|
559
|
+
openPositions: Object.keys(this.positions).length,
|
|
560
|
+
totalUnrealizedPnL: Object.values(unrealizedPnL).reduce((s, v) => s + v, 0),
|
|
561
|
+
topPositions,
|
|
562
|
+
recentTrades,
|
|
563
|
+
totalRealizedPnL
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// ============================================================
|
|
567
|
+
// INTERNAL
|
|
568
|
+
// ============================================================
|
|
569
|
+
persist() {
|
|
570
|
+
this.store.set(KEY_POSITIONS, this.positions);
|
|
571
|
+
this.store.set(KEY_TRADE_HISTORY, this.tradeHistory);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/llm/openai.ts
|
|
576
|
+
var import_openai = __toESM(require("openai"));
|
|
577
|
+
|
|
578
|
+
// src/llm/base.ts
|
|
579
|
+
var BaseLLMAdapter = class {
|
|
580
|
+
config;
|
|
581
|
+
constructor(config) {
|
|
582
|
+
this.config = config;
|
|
583
|
+
}
|
|
584
|
+
getMetadata() {
|
|
585
|
+
return {
|
|
586
|
+
provider: this.config.provider,
|
|
587
|
+
model: this.config.model || "unknown",
|
|
588
|
+
isLocal: this.config.provider === "ollama"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Format model name for display
|
|
593
|
+
*/
|
|
594
|
+
getDisplayModel() {
|
|
595
|
+
if (this.config.provider === "ollama") {
|
|
596
|
+
return `Local (${this.config.model || "ollama"})`;
|
|
597
|
+
}
|
|
598
|
+
return this.config.model || this.config.provider;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// src/llm/openai.ts
|
|
603
|
+
var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
604
|
+
client;
|
|
605
|
+
constructor(config) {
|
|
606
|
+
super(config);
|
|
607
|
+
if (!config.apiKey && !config.endpoint) {
|
|
608
|
+
throw new Error("OpenAI API key or custom endpoint required");
|
|
609
|
+
}
|
|
610
|
+
this.client = new import_openai.default({
|
|
611
|
+
apiKey: config.apiKey || "not-needed-for-custom",
|
|
612
|
+
baseURL: config.endpoint
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async chat(messages) {
|
|
616
|
+
try {
|
|
617
|
+
const response = await this.client.chat.completions.create({
|
|
618
|
+
model: this.config.model || "gpt-4.1",
|
|
619
|
+
messages: messages.map((m) => ({
|
|
620
|
+
role: m.role,
|
|
621
|
+
content: m.content
|
|
622
|
+
})),
|
|
623
|
+
temperature: this.config.temperature,
|
|
624
|
+
max_tokens: this.config.maxTokens
|
|
625
|
+
});
|
|
626
|
+
const choice = response.choices[0];
|
|
627
|
+
if (!choice || !choice.message) {
|
|
628
|
+
throw new Error("No response from OpenAI");
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
content: choice.message.content || "",
|
|
632
|
+
usage: response.usage ? {
|
|
633
|
+
promptTokens: response.usage.prompt_tokens,
|
|
634
|
+
completionTokens: response.usage.completion_tokens,
|
|
635
|
+
totalTokens: response.usage.total_tokens
|
|
636
|
+
} : void 0
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error instanceof import_openai.default.APIError) {
|
|
640
|
+
throw new Error(`OpenAI API error: ${error.message}`);
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/llm/anthropic.ts
|
|
648
|
+
var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
649
|
+
apiKey;
|
|
650
|
+
baseUrl;
|
|
651
|
+
constructor(config) {
|
|
652
|
+
super(config);
|
|
653
|
+
if (!config.apiKey) {
|
|
654
|
+
throw new Error("Anthropic API key required");
|
|
655
|
+
}
|
|
656
|
+
this.apiKey = config.apiKey;
|
|
657
|
+
this.baseUrl = config.endpoint || "https://api.anthropic.com";
|
|
658
|
+
}
|
|
659
|
+
async chat(messages) {
|
|
660
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
661
|
+
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
662
|
+
const body = {
|
|
663
|
+
model: this.config.model || "claude-opus-4-5-20251101",
|
|
664
|
+
max_tokens: this.config.maxTokens || 4096,
|
|
665
|
+
temperature: this.config.temperature,
|
|
666
|
+
system: systemMessage?.content,
|
|
667
|
+
messages: chatMessages.map((m) => ({
|
|
668
|
+
role: m.role,
|
|
133
669
|
content: m.content
|
|
134
670
|
}))
|
|
135
671
|
};
|
|
@@ -1150,349 +1686,76 @@ function classifyTradeError(message) {
|
|
|
1150
1686
|
};
|
|
1151
1687
|
}
|
|
1152
1688
|
|
|
1153
|
-
// src/trading/
|
|
1154
|
-
var
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
"
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
1165
|
-
// DAI
|
|
1166
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
1167
|
-
// cbETH
|
|
1168
|
-
[NATIVE_ETH.toLowerCase()]: 18,
|
|
1169
|
-
// Native ETH
|
|
1170
|
-
// Base Mainnet — Established tokens
|
|
1171
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
1172
|
-
// AERO (Aerodrome)
|
|
1173
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
1174
|
-
// BRETT
|
|
1175
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
1176
|
-
// DEGEN
|
|
1177
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
1178
|
-
// VIRTUAL
|
|
1179
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
1180
|
-
// TOSHI
|
|
1181
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
1182
|
-
// cbBTC
|
|
1183
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
1184
|
-
// ezETH (Renzo)
|
|
1185
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
1186
|
-
// wstETH (Lido)
|
|
1187
|
-
};
|
|
1188
|
-
function getTokenDecimals(address) {
|
|
1189
|
-
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
1190
|
-
if (decimals === void 0) {
|
|
1191
|
-
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
1192
|
-
return 18;
|
|
1193
|
-
}
|
|
1194
|
-
return decimals;
|
|
1195
|
-
}
|
|
1196
|
-
var TOKEN_TO_COINGECKO = {
|
|
1197
|
-
// Core
|
|
1198
|
-
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
1199
|
-
// WETH
|
|
1200
|
-
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
1201
|
-
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
1202
|
-
// USDC
|
|
1203
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
1204
|
-
// USDbC
|
|
1205
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
1206
|
-
// cbETH
|
|
1207
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
1208
|
-
// DAI
|
|
1209
|
-
// Established
|
|
1210
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
1211
|
-
// AERO
|
|
1212
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
1213
|
-
// BRETT
|
|
1214
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
1215
|
-
// DEGEN
|
|
1216
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
1217
|
-
// VIRTUAL
|
|
1218
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
1219
|
-
// TOSHI
|
|
1220
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
1221
|
-
// cbBTC
|
|
1222
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
1223
|
-
// ezETH
|
|
1224
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
1225
|
-
// wstETH
|
|
1226
|
-
};
|
|
1227
|
-
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
1228
|
-
var PRICE_STALENESS_MS = 6e4;
|
|
1229
|
-
var MarketDataService = class {
|
|
1230
|
-
rpcUrl;
|
|
1231
|
-
client;
|
|
1232
|
-
/** Cached prices from last fetch */
|
|
1233
|
-
cachedPrices = {};
|
|
1234
|
-
/** Timestamp of last successful price fetch */
|
|
1235
|
-
lastPriceFetchAt = 0;
|
|
1236
|
-
constructor(rpcUrl) {
|
|
1237
|
-
this.rpcUrl = rpcUrl;
|
|
1238
|
-
this.client = (0, import_viem.createPublicClient)({
|
|
1239
|
-
transport: (0, import_viem.http)(rpcUrl)
|
|
1240
|
-
});
|
|
1689
|
+
// src/trading/risk.ts
|
|
1690
|
+
var RiskManager = class {
|
|
1691
|
+
config;
|
|
1692
|
+
dailyPnL = 0;
|
|
1693
|
+
dailyFees = 0;
|
|
1694
|
+
lastResetDate = "";
|
|
1695
|
+
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1696
|
+
minTradeValueUSD;
|
|
1697
|
+
constructor(config) {
|
|
1698
|
+
this.config = config;
|
|
1699
|
+
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1241
1700
|
}
|
|
1242
|
-
/** Cached volume data */
|
|
1243
|
-
cachedVolume24h = {};
|
|
1244
|
-
/** Cached price change data */
|
|
1245
|
-
cachedPriceChange24h = {};
|
|
1246
1701
|
/**
|
|
1247
|
-
*
|
|
1702
|
+
* Filter signals through risk checks
|
|
1703
|
+
* Returns only signals that pass all guardrails
|
|
1248
1704
|
*/
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
gasPrice = await this.client.getGasPrice();
|
|
1256
|
-
} catch {
|
|
1705
|
+
filterSignals(signals, marketData) {
|
|
1706
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1707
|
+
if (today !== this.lastResetDate) {
|
|
1708
|
+
this.dailyPnL = 0;
|
|
1709
|
+
this.dailyFees = 0;
|
|
1710
|
+
this.lastResetDate = today;
|
|
1257
1711
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1264
|
-
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1265
|
-
gasPrice,
|
|
1266
|
-
network: {
|
|
1267
|
-
chainId: this.client.chain?.id ?? 8453
|
|
1268
|
-
}
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Check if cached prices are still fresh
|
|
1273
|
-
*/
|
|
1274
|
-
get pricesAreFresh() {
|
|
1275
|
-
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
1712
|
+
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1713
|
+
console.warn("Daily loss limit reached - no new trades");
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1276
1717
|
}
|
|
1277
1718
|
/**
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1719
|
+
* Validate individual signal against risk limits.
|
|
1720
|
+
* Sell signals are exempt from position size and minimum value checks —
|
|
1721
|
+
* those guardrails prevent oversized/dust buys, but blocking exits traps capital.
|
|
1280
1722
|
*/
|
|
1281
|
-
|
|
1282
|
-
if (
|
|
1283
|
-
|
|
1284
|
-
for (const addr of tokenAddresses) {
|
|
1285
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1286
|
-
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1287
|
-
prices2[addr.toLowerCase()] = 1;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
return prices2;
|
|
1723
|
+
validateSignal(signal, marketData) {
|
|
1724
|
+
if (signal.action === "hold") {
|
|
1725
|
+
return true;
|
|
1291
1726
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1296
|
-
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1297
|
-
idsToFetch.add(cgId);
|
|
1298
|
-
}
|
|
1727
|
+
if (signal.confidence < 0.5) {
|
|
1728
|
+
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1729
|
+
return false;
|
|
1299
1730
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
try {
|
|
1303
|
-
const ids = Array.from(idsToFetch).join(",");
|
|
1304
|
-
const response = await fetch(
|
|
1305
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1306
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1307
|
-
);
|
|
1308
|
-
if (response.ok) {
|
|
1309
|
-
const data = await response.json();
|
|
1310
|
-
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1311
|
-
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1312
|
-
if (id === cgId) {
|
|
1313
|
-
const key = addr.toLowerCase();
|
|
1314
|
-
prices[key] = priceData.usd;
|
|
1315
|
-
if (priceData.usd_24h_vol !== void 0) {
|
|
1316
|
-
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1317
|
-
}
|
|
1318
|
-
if (priceData.usd_24h_change !== void 0) {
|
|
1319
|
-
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
this.lastPriceFetchAt = Date.now();
|
|
1325
|
-
} else {
|
|
1326
|
-
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1327
|
-
}
|
|
1328
|
-
} catch (error) {
|
|
1329
|
-
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1330
|
-
}
|
|
1731
|
+
if (signal.action === "sell") {
|
|
1732
|
+
return true;
|
|
1331
1733
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1734
|
+
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1735
|
+
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1736
|
+
if (signalValue > maxPositionValue) {
|
|
1737
|
+
console.warn(
|
|
1738
|
+
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1739
|
+
);
|
|
1740
|
+
return false;
|
|
1337
1741
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1344
|
-
const llamaResponse = await fetch(
|
|
1345
|
-
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1346
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1742
|
+
if (this.config.maxConcurrentPositions) {
|
|
1743
|
+
const activePositions = this.countActivePositions(marketData);
|
|
1744
|
+
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1745
|
+
console.warn(
|
|
1746
|
+
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1347
1747
|
);
|
|
1348
|
-
|
|
1349
|
-
const llamaData = await llamaResponse.json();
|
|
1350
|
-
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1351
|
-
const addr = key.replace("base:", "").toLowerCase();
|
|
1352
|
-
if (data.price && data.confidence > 0.5) {
|
|
1353
|
-
prices[addr] = data.price;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1357
|
-
}
|
|
1358
|
-
} catch {
|
|
1748
|
+
return false;
|
|
1359
1749
|
}
|
|
1360
1750
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
this.
|
|
1363
|
-
|
|
1364
|
-
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1365
|
-
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1366
|
-
return { ...this.cachedPrices };
|
|
1367
|
-
}
|
|
1368
|
-
for (const addr of tokenAddresses) {
|
|
1369
|
-
if (!prices[addr.toLowerCase()]) {
|
|
1370
|
-
console.warn(`No price available for ${addr}, using 0`);
|
|
1371
|
-
prices[addr.toLowerCase()] = 0;
|
|
1372
|
-
}
|
|
1751
|
+
if (signalValue < this.minTradeValueUSD) {
|
|
1752
|
+
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1753
|
+
return false;
|
|
1373
1754
|
}
|
|
1374
|
-
return
|
|
1755
|
+
return true;
|
|
1375
1756
|
}
|
|
1376
1757
|
/**
|
|
1377
|
-
*
|
|
1378
|
-
*/
|
|
1379
|
-
async fetchBalances(walletAddress, tokenAddresses) {
|
|
1380
|
-
const balances = {};
|
|
1381
|
-
const wallet = walletAddress;
|
|
1382
|
-
try {
|
|
1383
|
-
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
1384
|
-
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
1385
|
-
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
1386
|
-
try {
|
|
1387
|
-
const balance = await this.client.readContract({
|
|
1388
|
-
address: tokenAddress,
|
|
1389
|
-
abi: import_viem.erc20Abi,
|
|
1390
|
-
functionName: "balanceOf",
|
|
1391
|
-
args: [wallet]
|
|
1392
|
-
});
|
|
1393
|
-
return { address: tokenAddress.toLowerCase(), balance };
|
|
1394
|
-
} catch (error) {
|
|
1395
|
-
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
1396
|
-
}
|
|
1397
|
-
});
|
|
1398
|
-
const results = await Promise.all(erc20Promises);
|
|
1399
|
-
for (const { address, balance } of results) {
|
|
1400
|
-
balances[address] = balance;
|
|
1401
|
-
}
|
|
1402
|
-
} catch (error) {
|
|
1403
|
-
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
1404
|
-
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
1405
|
-
for (const address of tokenAddresses) {
|
|
1406
|
-
balances[address.toLowerCase()] = 0n;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
return balances;
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Calculate total portfolio value in USD
|
|
1413
|
-
*/
|
|
1414
|
-
calculatePortfolioValue(balances, prices) {
|
|
1415
|
-
let total = 0;
|
|
1416
|
-
for (const [address, balance] of Object.entries(balances)) {
|
|
1417
|
-
const price = prices[address.toLowerCase()] || 0;
|
|
1418
|
-
const decimals = getTokenDecimals(address);
|
|
1419
|
-
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1420
|
-
total += amount * price;
|
|
1421
|
-
}
|
|
1422
|
-
return total;
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
1425
|
-
|
|
1426
|
-
// src/trading/risk.ts
|
|
1427
|
-
var RiskManager = class {
|
|
1428
|
-
config;
|
|
1429
|
-
dailyPnL = 0;
|
|
1430
|
-
dailyFees = 0;
|
|
1431
|
-
lastResetDate = "";
|
|
1432
|
-
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1433
|
-
minTradeValueUSD;
|
|
1434
|
-
constructor(config) {
|
|
1435
|
-
this.config = config;
|
|
1436
|
-
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Filter signals through risk checks
|
|
1440
|
-
* Returns only signals that pass all guardrails
|
|
1441
|
-
*/
|
|
1442
|
-
filterSignals(signals, marketData) {
|
|
1443
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1444
|
-
if (today !== this.lastResetDate) {
|
|
1445
|
-
this.dailyPnL = 0;
|
|
1446
|
-
this.dailyFees = 0;
|
|
1447
|
-
this.lastResetDate = today;
|
|
1448
|
-
}
|
|
1449
|
-
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1450
|
-
console.warn("Daily loss limit reached - no new trades");
|
|
1451
|
-
return [];
|
|
1452
|
-
}
|
|
1453
|
-
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* Validate individual signal against risk limits.
|
|
1457
|
-
* Sell signals are exempt from position size and minimum value checks —
|
|
1458
|
-
* those guardrails prevent oversized/dust buys, but blocking exits traps capital.
|
|
1459
|
-
*/
|
|
1460
|
-
validateSignal(signal, marketData) {
|
|
1461
|
-
if (signal.action === "hold") {
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1464
|
-
if (signal.confidence < 0.5) {
|
|
1465
|
-
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1466
|
-
return false;
|
|
1467
|
-
}
|
|
1468
|
-
if (signal.action === "sell") {
|
|
1469
|
-
return true;
|
|
1470
|
-
}
|
|
1471
|
-
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1472
|
-
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1473
|
-
if (signalValue > maxPositionValue) {
|
|
1474
|
-
console.warn(
|
|
1475
|
-
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1476
|
-
);
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
if (this.config.maxConcurrentPositions) {
|
|
1480
|
-
const activePositions = this.countActivePositions(marketData);
|
|
1481
|
-
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1482
|
-
console.warn(
|
|
1483
|
-
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1484
|
-
);
|
|
1485
|
-
return false;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
if (signalValue < this.minTradeValueUSD) {
|
|
1489
|
-
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1490
|
-
return false;
|
|
1491
|
-
}
|
|
1492
|
-
return true;
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1758
|
+
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1496
1759
|
*/
|
|
1497
1760
|
countActivePositions(marketData) {
|
|
1498
1761
|
const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
@@ -1533,6 +1796,31 @@ var RiskManager = class {
|
|
|
1533
1796
|
updateFees(fees) {
|
|
1534
1797
|
this.dailyFees += fees;
|
|
1535
1798
|
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Export current risk state for persistence across restarts.
|
|
1801
|
+
*/
|
|
1802
|
+
exportState() {
|
|
1803
|
+
return {
|
|
1804
|
+
dailyPnL: this.dailyPnL,
|
|
1805
|
+
dailyFees: this.dailyFees,
|
|
1806
|
+
lastResetDate: this.lastResetDate
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Restore risk state from persistence (called on startup).
|
|
1811
|
+
* Only restores if the saved state is from today — expired state is ignored.
|
|
1812
|
+
*/
|
|
1813
|
+
restoreState(state) {
|
|
1814
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1815
|
+
if (state.lastResetDate === today) {
|
|
1816
|
+
this.dailyPnL = state.dailyPnL;
|
|
1817
|
+
this.dailyFees = state.dailyFees;
|
|
1818
|
+
this.lastResetDate = state.lastResetDate;
|
|
1819
|
+
console.log(`Risk state restored: PnL=$${this.dailyPnL.toFixed(2)}, Fees=$${this.dailyFees.toFixed(2)}`);
|
|
1820
|
+
} else {
|
|
1821
|
+
console.log("Risk state expired (different day), starting fresh");
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1536
1824
|
/**
|
|
1537
1825
|
* Get current risk status
|
|
1538
1826
|
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
@@ -1994,300 +2282,37 @@ var VaultManager = class {
|
|
|
1994
2282
|
};
|
|
1995
2283
|
|
|
1996
2284
|
// src/relay.ts
|
|
1997
|
-
var
|
|
1998
|
-
var
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
reconnectAttempts = 0;
|
|
2006
|
-
maxReconnectAttempts = 50;
|
|
2007
|
-
reconnectTimer = null;
|
|
2008
|
-
heartbeatTimer = null;
|
|
2009
|
-
stopped = false;
|
|
2285
|
+
var import_ws2 = __toESM(require("ws"));
|
|
2286
|
+
var import_accounts4 = require("viem/accounts");
|
|
2287
|
+
|
|
2288
|
+
// src/perp/client.ts
|
|
2289
|
+
var HyperliquidClient = class {
|
|
2290
|
+
apiUrl;
|
|
2291
|
+
meta = null;
|
|
2292
|
+
assetIndexCache = /* @__PURE__ */ new Map();
|
|
2010
2293
|
constructor(config) {
|
|
2011
|
-
this.
|
|
2294
|
+
this.apiUrl = config.apiUrl;
|
|
2012
2295
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
console.error("Relay: Failed to create WebSocket:", error);
|
|
2024
|
-
this.scheduleReconnect();
|
|
2025
|
-
reject(error);
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
const connectTimeout = setTimeout(() => {
|
|
2029
|
-
if (!this.authenticated) {
|
|
2030
|
-
console.error("Relay: Connection timeout");
|
|
2031
|
-
this.ws?.close();
|
|
2032
|
-
this.scheduleReconnect();
|
|
2033
|
-
reject(new Error("Connection timeout"));
|
|
2034
|
-
}
|
|
2035
|
-
}, 15e3);
|
|
2036
|
-
this.ws.on("open", async () => {
|
|
2037
|
-
this.authRejected = false;
|
|
2038
|
-
console.log("Relay: Connected, authenticating...");
|
|
2039
|
-
try {
|
|
2040
|
-
await this.authenticate();
|
|
2041
|
-
} catch (error) {
|
|
2042
|
-
console.error("Relay: Authentication failed:", error);
|
|
2043
|
-
this.ws?.close();
|
|
2044
|
-
clearTimeout(connectTimeout);
|
|
2045
|
-
reject(error);
|
|
2046
|
-
}
|
|
2047
|
-
});
|
|
2048
|
-
this.ws.on("message", (raw) => {
|
|
2049
|
-
try {
|
|
2050
|
-
const data = JSON.parse(raw.toString());
|
|
2051
|
-
this.handleMessage(data);
|
|
2052
|
-
if (data.type === "auth_success") {
|
|
2053
|
-
clearTimeout(connectTimeout);
|
|
2054
|
-
this.authenticated = true;
|
|
2055
|
-
this.reconnectAttempts = 0;
|
|
2056
|
-
this.startHeartbeat();
|
|
2057
|
-
console.log("Relay: Authenticated successfully");
|
|
2058
|
-
resolve();
|
|
2059
|
-
} else if (data.type === "auth_error") {
|
|
2060
|
-
clearTimeout(connectTimeout);
|
|
2061
|
-
this.authRejected = true;
|
|
2062
|
-
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
2063
|
-
reject(new Error(data.message));
|
|
2064
|
-
}
|
|
2065
|
-
} catch {
|
|
2066
|
-
}
|
|
2067
|
-
});
|
|
2068
|
-
this.ws.on("close", (code, reason) => {
|
|
2069
|
-
clearTimeout(connectTimeout);
|
|
2070
|
-
this.authenticated = false;
|
|
2071
|
-
this.stopHeartbeat();
|
|
2072
|
-
if (!this.stopped) {
|
|
2073
|
-
if (!this.authRejected) {
|
|
2074
|
-
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
2075
|
-
}
|
|
2076
|
-
this.scheduleReconnect();
|
|
2077
|
-
}
|
|
2078
|
-
});
|
|
2079
|
-
this.ws.on("error", (error) => {
|
|
2080
|
-
if (!this.stopped) {
|
|
2081
|
-
console.error("Relay: WebSocket error:", error.message);
|
|
2082
|
-
}
|
|
2083
|
-
});
|
|
2296
|
+
// ============================================================
|
|
2297
|
+
// INFO API (read-only)
|
|
2298
|
+
// ============================================================
|
|
2299
|
+
/** Fetch perpetuals metadata (asset specs, names, indices) */
|
|
2300
|
+
async getMeta() {
|
|
2301
|
+
if (this.meta) return this.meta;
|
|
2302
|
+
const resp = await this.infoRequest({ type: "meta" });
|
|
2303
|
+
this.meta = resp.universe;
|
|
2304
|
+
this.meta.forEach((asset, idx) => {
|
|
2305
|
+
this.assetIndexCache.set(asset.name, idx);
|
|
2084
2306
|
});
|
|
2307
|
+
return this.meta;
|
|
2085
2308
|
}
|
|
2086
|
-
/**
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
const signature = await (0, import_accounts2.signMessage)({
|
|
2094
|
-
message,
|
|
2095
|
-
privateKey: this.config.privateKey
|
|
2096
|
-
});
|
|
2097
|
-
this.send({
|
|
2098
|
-
type: "auth",
|
|
2099
|
-
agentId: this.config.agentId,
|
|
2100
|
-
wallet: account.address,
|
|
2101
|
-
timestamp,
|
|
2102
|
-
signature,
|
|
2103
|
-
sdkVersion: import_sdk.SDK_VERSION
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
/**
|
|
2107
|
-
* Handle incoming messages from the relay server
|
|
2108
|
-
*/
|
|
2109
|
-
handleMessage(data) {
|
|
2110
|
-
switch (data.type) {
|
|
2111
|
-
case "command":
|
|
2112
|
-
if (data.command && this.config.onCommand) {
|
|
2113
|
-
this.config.onCommand(data.command);
|
|
2114
|
-
}
|
|
2115
|
-
break;
|
|
2116
|
-
case "auth_success":
|
|
2117
|
-
case "auth_error":
|
|
2118
|
-
break;
|
|
2119
|
-
case "error":
|
|
2120
|
-
console.error(`Relay: Server error: ${data.message}`);
|
|
2121
|
-
break;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
/**
|
|
2125
|
-
* Send a status heartbeat
|
|
2126
|
-
*/
|
|
2127
|
-
sendHeartbeat(status) {
|
|
2128
|
-
if (!this.authenticated) return;
|
|
2129
|
-
this.send({
|
|
2130
|
-
type: "heartbeat",
|
|
2131
|
-
agentId: this.config.agentId,
|
|
2132
|
-
status
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
/**
|
|
2136
|
-
* Send a status update (outside of regular heartbeat)
|
|
2137
|
-
*/
|
|
2138
|
-
sendStatusUpdate(status) {
|
|
2139
|
-
if (!this.authenticated) return;
|
|
2140
|
-
this.send({
|
|
2141
|
-
type: "status_update",
|
|
2142
|
-
agentId: this.config.agentId,
|
|
2143
|
-
status
|
|
2144
|
-
});
|
|
2145
|
-
}
|
|
2146
|
-
/**
|
|
2147
|
-
* Send a message to the command center
|
|
2148
|
-
*/
|
|
2149
|
-
sendMessage(messageType, level, title, body, data) {
|
|
2150
|
-
if (!this.authenticated) return;
|
|
2151
|
-
this.send({
|
|
2152
|
-
type: "message",
|
|
2153
|
-
agentId: this.config.agentId,
|
|
2154
|
-
messageType,
|
|
2155
|
-
level,
|
|
2156
|
-
title,
|
|
2157
|
-
body,
|
|
2158
|
-
data
|
|
2159
|
-
});
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Send a command execution result
|
|
2163
|
-
*/
|
|
2164
|
-
sendCommandResult(commandId, success, result) {
|
|
2165
|
-
if (!this.authenticated) return;
|
|
2166
|
-
this.send({
|
|
2167
|
-
type: "command_result",
|
|
2168
|
-
agentId: this.config.agentId,
|
|
2169
|
-
commandId,
|
|
2170
|
-
success,
|
|
2171
|
-
result
|
|
2172
|
-
});
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Start the heartbeat timer
|
|
2176
|
-
*/
|
|
2177
|
-
startHeartbeat() {
|
|
2178
|
-
this.stopHeartbeat();
|
|
2179
|
-
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
2180
|
-
this.heartbeatTimer = setInterval(() => {
|
|
2181
|
-
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2182
|
-
this.ws.ping();
|
|
2183
|
-
}
|
|
2184
|
-
}, interval);
|
|
2185
|
-
}
|
|
2186
|
-
/**
|
|
2187
|
-
* Stop the heartbeat timer
|
|
2188
|
-
*/
|
|
2189
|
-
stopHeartbeat() {
|
|
2190
|
-
if (this.heartbeatTimer) {
|
|
2191
|
-
clearInterval(this.heartbeatTimer);
|
|
2192
|
-
this.heartbeatTimer = null;
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
/**
|
|
2196
|
-
* Schedule a reconnection with exponential backoff
|
|
2197
|
-
*/
|
|
2198
|
-
scheduleReconnect() {
|
|
2199
|
-
if (this.stopped) return;
|
|
2200
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
2201
|
-
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
2202
|
-
return;
|
|
2203
|
-
}
|
|
2204
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
2205
|
-
this.reconnectAttempts++;
|
|
2206
|
-
console.log(
|
|
2207
|
-
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
2208
|
-
);
|
|
2209
|
-
this.reconnectTimer = setTimeout(() => {
|
|
2210
|
-
this.connect().catch(() => {
|
|
2211
|
-
});
|
|
2212
|
-
}, delay);
|
|
2213
|
-
}
|
|
2214
|
-
/**
|
|
2215
|
-
* Send a JSON message to the WebSocket
|
|
2216
|
-
*/
|
|
2217
|
-
send(data) {
|
|
2218
|
-
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
2219
|
-
this.ws.send(JSON.stringify(data));
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* Check if connected and authenticated
|
|
2224
|
-
*/
|
|
2225
|
-
get isConnected() {
|
|
2226
|
-
return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
|
|
2227
|
-
}
|
|
2228
|
-
/**
|
|
2229
|
-
* Disconnect and stop reconnecting
|
|
2230
|
-
*/
|
|
2231
|
-
disconnect() {
|
|
2232
|
-
this.stopped = true;
|
|
2233
|
-
this.stopHeartbeat();
|
|
2234
|
-
if (this.reconnectTimer) {
|
|
2235
|
-
clearTimeout(this.reconnectTimer);
|
|
2236
|
-
this.reconnectTimer = null;
|
|
2237
|
-
}
|
|
2238
|
-
if (this.ws) {
|
|
2239
|
-
this.ws.close(1e3, "Agent shutting down");
|
|
2240
|
-
this.ws = null;
|
|
2241
|
-
}
|
|
2242
|
-
this.authenticated = false;
|
|
2243
|
-
console.log("Relay: Disconnected");
|
|
2244
|
-
}
|
|
2245
|
-
};
|
|
2246
|
-
|
|
2247
|
-
// src/browser-open.ts
|
|
2248
|
-
var import_child_process2 = require("child_process");
|
|
2249
|
-
function openBrowser(url) {
|
|
2250
|
-
const platform = process.platform;
|
|
2251
|
-
try {
|
|
2252
|
-
if (platform === "darwin") {
|
|
2253
|
-
(0, import_child_process2.exec)(`open "${url}"`);
|
|
2254
|
-
} else if (platform === "win32") {
|
|
2255
|
-
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
2256
|
-
} else {
|
|
2257
|
-
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
2258
|
-
}
|
|
2259
|
-
} catch {
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
// src/perp/client.ts
|
|
2264
|
-
var HyperliquidClient = class {
|
|
2265
|
-
apiUrl;
|
|
2266
|
-
meta = null;
|
|
2267
|
-
assetIndexCache = /* @__PURE__ */ new Map();
|
|
2268
|
-
constructor(config) {
|
|
2269
|
-
this.apiUrl = config.apiUrl;
|
|
2270
|
-
}
|
|
2271
|
-
// ============================================================
|
|
2272
|
-
// INFO API (read-only)
|
|
2273
|
-
// ============================================================
|
|
2274
|
-
/** Fetch perpetuals metadata (asset specs, names, indices) */
|
|
2275
|
-
async getMeta() {
|
|
2276
|
-
if (this.meta) return this.meta;
|
|
2277
|
-
const resp = await this.infoRequest({ type: "meta" });
|
|
2278
|
-
this.meta = resp.universe;
|
|
2279
|
-
this.meta.forEach((asset, idx) => {
|
|
2280
|
-
this.assetIndexCache.set(asset.name, idx);
|
|
2281
|
-
});
|
|
2282
|
-
return this.meta;
|
|
2283
|
-
}
|
|
2284
|
-
/** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
|
|
2285
|
-
async getAssetIndex(coin) {
|
|
2286
|
-
if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
|
|
2287
|
-
await this.getMeta();
|
|
2288
|
-
const idx = this.assetIndexCache.get(coin);
|
|
2289
|
-
if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
|
|
2290
|
-
return idx;
|
|
2309
|
+
/** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
|
|
2310
|
+
async getAssetIndex(coin) {
|
|
2311
|
+
if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
|
|
2312
|
+
await this.getMeta();
|
|
2313
|
+
const idx = this.assetIndexCache.get(coin);
|
|
2314
|
+
if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
|
|
2315
|
+
return idx;
|
|
2291
2316
|
}
|
|
2292
2317
|
/** Get mid-market prices for all perpetuals */
|
|
2293
2318
|
async getAllMids() {
|
|
@@ -2864,7 +2889,7 @@ var PositionManager = class {
|
|
|
2864
2889
|
};
|
|
2865
2890
|
|
|
2866
2891
|
// src/perp/websocket.ts
|
|
2867
|
-
var
|
|
2892
|
+
var import_ws = __toESM(require("ws"));
|
|
2868
2893
|
var HyperliquidWebSocket = class {
|
|
2869
2894
|
wsUrl;
|
|
2870
2895
|
userAddress;
|
|
@@ -2896,14 +2921,14 @@ var HyperliquidWebSocket = class {
|
|
|
2896
2921
|
* Connect to Hyperliquid WebSocket and subscribe to user events.
|
|
2897
2922
|
*/
|
|
2898
2923
|
async connect() {
|
|
2899
|
-
if (this.ws?.readyState ===
|
|
2924
|
+
if (this.ws?.readyState === import_ws.default.OPEN || this.isConnecting) {
|
|
2900
2925
|
return;
|
|
2901
2926
|
}
|
|
2902
2927
|
this.isConnecting = true;
|
|
2903
2928
|
this.shouldReconnect = true;
|
|
2904
2929
|
return new Promise((resolve, reject) => {
|
|
2905
2930
|
try {
|
|
2906
|
-
this.ws = new
|
|
2931
|
+
this.ws = new import_ws.default(this.wsUrl);
|
|
2907
2932
|
this.ws.on("open", () => {
|
|
2908
2933
|
this.isConnecting = false;
|
|
2909
2934
|
this.reconnectAttempts = 0;
|
|
@@ -2956,7 +2981,7 @@ var HyperliquidWebSocket = class {
|
|
|
2956
2981
|
this.stopPing();
|
|
2957
2982
|
if (this.ws) {
|
|
2958
2983
|
this.ws.removeAllListeners();
|
|
2959
|
-
if (this.ws.readyState ===
|
|
2984
|
+
if (this.ws.readyState === import_ws.default.OPEN) {
|
|
2960
2985
|
this.ws.close(1e3, "Client disconnect");
|
|
2961
2986
|
}
|
|
2962
2987
|
this.ws = null;
|
|
@@ -2967,7 +2992,7 @@ var HyperliquidWebSocket = class {
|
|
|
2967
2992
|
* Check if WebSocket is connected.
|
|
2968
2993
|
*/
|
|
2969
2994
|
get isConnected() {
|
|
2970
|
-
return this.ws?.readyState ===
|
|
2995
|
+
return this.ws?.readyState === import_ws.default.OPEN;
|
|
2971
2996
|
}
|
|
2972
2997
|
// ============================================================
|
|
2973
2998
|
// EVENT HANDLERS
|
|
@@ -3094,7 +3119,7 @@ var HyperliquidWebSocket = class {
|
|
|
3094
3119
|
startPing() {
|
|
3095
3120
|
this.stopPing();
|
|
3096
3121
|
this.pingTimer = setInterval(() => {
|
|
3097
|
-
if (this.ws?.readyState ===
|
|
3122
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
3098
3123
|
this.ws.send(JSON.stringify({ method: "ping" }));
|
|
3099
3124
|
}
|
|
3100
3125
|
}, 25e3);
|
|
@@ -3109,7 +3134,7 @@ var HyperliquidWebSocket = class {
|
|
|
3109
3134
|
// HELPERS
|
|
3110
3135
|
// ============================================================
|
|
3111
3136
|
subscribe(msg) {
|
|
3112
|
-
if (this.ws?.readyState ===
|
|
3137
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
3113
3138
|
this.ws.send(JSON.stringify(msg));
|
|
3114
3139
|
}
|
|
3115
3140
|
}
|
|
@@ -3118,7 +3143,7 @@ var HyperliquidWebSocket = class {
|
|
|
3118
3143
|
// src/perp/recorder.ts
|
|
3119
3144
|
var import_viem4 = require("viem");
|
|
3120
3145
|
var import_chains2 = require("viem/chains");
|
|
3121
|
-
var
|
|
3146
|
+
var import_accounts2 = require("viem/accounts");
|
|
3122
3147
|
var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
|
|
3123
3148
|
var ROUTER_ABI = [
|
|
3124
3149
|
{
|
|
@@ -3157,7 +3182,7 @@ var PerpTradeRecorder = class {
|
|
|
3157
3182
|
constructor(opts) {
|
|
3158
3183
|
this.agentId = opts.agentId;
|
|
3159
3184
|
this.configHash = opts.configHash;
|
|
3160
|
-
this.account = (0,
|
|
3185
|
+
this.account = (0, import_accounts2.privateKeyToAccount)(opts.privateKey);
|
|
3161
3186
|
const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
|
|
3162
3187
|
const transport = (0, import_viem4.http)(rpcUrl);
|
|
3163
3188
|
this.publicClient = (0, import_viem4.createPublicClient)({
|
|
@@ -3265,232 +3290,683 @@ var PerpTradeRecorder = class {
|
|
|
3265
3290
|
}
|
|
3266
3291
|
}
|
|
3267
3292
|
/**
|
|
3268
|
-
* 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
|
|
3269
3816
|
*/
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
const idx = this.retryQueue.indexOf(item);
|
|
3284
|
-
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3285
|
-
continue;
|
|
3286
|
-
}
|
|
3287
|
-
console.log(
|
|
3288
|
-
`Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
|
|
3289
|
-
);
|
|
3290
|
-
const result = await this.submitRecord(item.params);
|
|
3291
|
-
if (result.success) {
|
|
3292
|
-
const idx = this.retryQueue.indexOf(item);
|
|
3293
|
-
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
3294
|
-
}
|
|
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;
|
|
3295
3830
|
}
|
|
3296
3831
|
}
|
|
3297
|
-
// ============================================================
|
|
3298
|
-
// CONVERSION HELPERS
|
|
3299
|
-
// ============================================================
|
|
3300
3832
|
/**
|
|
3301
|
-
*
|
|
3302
|
-
* notionalUSD = px * sz * 1e6
|
|
3833
|
+
* Send a status heartbeat
|
|
3303
3834
|
*/
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3835
|
+
sendHeartbeat(status) {
|
|
3836
|
+
if (!this.authenticated) return;
|
|
3837
|
+
this.send({
|
|
3838
|
+
type: "heartbeat",
|
|
3839
|
+
agentId: this.config.agentId,
|
|
3840
|
+
status
|
|
3841
|
+
});
|
|
3308
3842
|
}
|
|
3309
3843
|
/**
|
|
3310
|
-
*
|
|
3311
|
-
* feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
|
|
3844
|
+
* Send a status update (outside of regular heartbeat)
|
|
3312
3845
|
*/
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
// src/perp/onboarding.ts
|
|
3321
|
-
var PerpOnboarding = class {
|
|
3322
|
-
client;
|
|
3323
|
-
signer;
|
|
3324
|
-
config;
|
|
3325
|
-
constructor(client, signer, config) {
|
|
3326
|
-
this.client = client;
|
|
3327
|
-
this.signer = signer;
|
|
3328
|
-
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
|
+
});
|
|
3329
3853
|
}
|
|
3330
|
-
// ============================================================
|
|
3331
|
-
// BUILDER FEE
|
|
3332
|
-
// ============================================================
|
|
3333
3854
|
/**
|
|
3334
|
-
*
|
|
3335
|
-
* Builder fee must be approved before orders can include builder fees.
|
|
3855
|
+
* Send a message to the command center
|
|
3336
3856
|
*/
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
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
|
+
});
|
|
3347
3868
|
}
|
|
3348
3869
|
/**
|
|
3349
|
-
*
|
|
3350
|
-
* This is a one-time approval per builder address.
|
|
3870
|
+
* Send a command execution result
|
|
3351
3871
|
*/
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
const { signature } = await this.signer.signApproval(action);
|
|
3362
|
-
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
3363
|
-
method: "POST",
|
|
3364
|
-
headers: { "Content-Type": "application/json" },
|
|
3365
|
-
body: JSON.stringify({
|
|
3366
|
-
action,
|
|
3367
|
-
signature: {
|
|
3368
|
-
r: signature.slice(0, 66),
|
|
3369
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
3370
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
3371
|
-
},
|
|
3372
|
-
nonce: action.nonce,
|
|
3373
|
-
vaultAddress: null
|
|
3374
|
-
})
|
|
3375
|
-
});
|
|
3376
|
-
if (!resp.ok) {
|
|
3377
|
-
const text = await resp.text();
|
|
3378
|
-
console.error(`Builder fee approval failed: ${resp.status} ${text}`);
|
|
3379
|
-
return false;
|
|
3380
|
-
}
|
|
3381
|
-
console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
|
|
3382
|
-
return true;
|
|
3383
|
-
} catch (error) {
|
|
3384
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3385
|
-
console.error(`Builder fee approval failed: ${message}`);
|
|
3386
|
-
return false;
|
|
3387
|
-
}
|
|
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
|
+
});
|
|
3388
3881
|
}
|
|
3389
|
-
// ============================================================
|
|
3390
|
-
// BALANCE & REQUIREMENTS
|
|
3391
|
-
// ============================================================
|
|
3392
3882
|
/**
|
|
3393
|
-
*
|
|
3394
|
-
* Returns the account equity in USD.
|
|
3883
|
+
* Start the heartbeat timer
|
|
3395
3884
|
*/
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
return { hasBalance: false, equity: 0 };
|
|
3405
|
-
}
|
|
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);
|
|
3406
3893
|
}
|
|
3407
3894
|
/**
|
|
3408
|
-
*
|
|
3409
|
-
* Perps require risk universe >= 2 (Derivatives or higher).
|
|
3895
|
+
* Stop the heartbeat timer
|
|
3410
3896
|
*/
|
|
3411
|
-
|
|
3412
|
-
if (
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
message: `Risk universe ${riskUniverse} allows perp trading`
|
|
3416
|
-
};
|
|
3897
|
+
stopHeartbeat() {
|
|
3898
|
+
if (this.heartbeatTimer) {
|
|
3899
|
+
clearInterval(this.heartbeatTimer);
|
|
3900
|
+
this.heartbeatTimer = null;
|
|
3417
3901
|
}
|
|
3418
|
-
return {
|
|
3419
|
-
allowed: false,
|
|
3420
|
-
message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
|
|
3421
|
-
};
|
|
3422
3902
|
}
|
|
3423
|
-
// ============================================================
|
|
3424
|
-
// FULL ONBOARDING CHECK
|
|
3425
|
-
// ============================================================
|
|
3426
3903
|
/**
|
|
3427
|
-
*
|
|
3428
|
-
* Does NOT auto-approve — caller must explicitly approve after review.
|
|
3904
|
+
* Schedule a reconnection with exponential backoff
|
|
3429
3905
|
*/
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
};
|
|
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);
|
|
3445
3921
|
}
|
|
3446
3922
|
/**
|
|
3447
|
-
*
|
|
3448
|
-
* Returns the final status after all actions.
|
|
3923
|
+
* Send a JSON message to the WebSocket
|
|
3449
3924
|
*/
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
|
|
3454
|
-
return status;
|
|
3455
|
-
}
|
|
3456
|
-
if (!status.hasBalance) {
|
|
3457
|
-
console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
|
|
3458
|
-
return status;
|
|
3925
|
+
send(data) {
|
|
3926
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
3927
|
+
this.ws.send(JSON.stringify(data));
|
|
3459
3928
|
}
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
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;
|
|
3466
3945
|
}
|
|
3467
|
-
if (
|
|
3468
|
-
|
|
3946
|
+
if (this.ws) {
|
|
3947
|
+
this.ws.close(1e3, "Agent shutting down");
|
|
3948
|
+
this.ws = null;
|
|
3469
3949
|
}
|
|
3470
|
-
|
|
3950
|
+
this.authenticated = false;
|
|
3951
|
+
console.log("Relay: Disconnected");
|
|
3471
3952
|
}
|
|
3472
3953
|
};
|
|
3473
3954
|
|
|
3474
|
-
// src/
|
|
3475
|
-
var
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
"event MessageSent(bytes message)"
|
|
3490
|
-
]);
|
|
3491
|
-
var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
|
|
3492
|
-
"function deposit(uint256 amount, uint32 destinationDex) external"
|
|
3493
|
-
]);
|
|
3955
|
+
// src/browser-open.ts
|
|
3956
|
+
var import_child_process2 = require("child_process");
|
|
3957
|
+
function openBrowser(url) {
|
|
3958
|
+
const platform = process.platform;
|
|
3959
|
+
try {
|
|
3960
|
+
if (platform === "darwin") {
|
|
3961
|
+
(0, import_child_process2.exec)(`open "${url}"`);
|
|
3962
|
+
} else if (platform === "win32") {
|
|
3963
|
+
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
3964
|
+
} else {
|
|
3965
|
+
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
3966
|
+
}
|
|
3967
|
+
} catch {
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3494
3970
|
|
|
3495
3971
|
// src/runtime.ts
|
|
3496
3972
|
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
@@ -3512,10 +3988,12 @@ var AgentRuntime = class {
|
|
|
3512
3988
|
lastCycleAt = 0;
|
|
3513
3989
|
lastPortfolioValue = 0;
|
|
3514
3990
|
lastEthBalance = "0";
|
|
3991
|
+
lastPrices = {};
|
|
3515
3992
|
processAlive = true;
|
|
3516
3993
|
riskUniverse = 0;
|
|
3517
3994
|
allowedTokens = /* @__PURE__ */ new Set();
|
|
3518
3995
|
strategyContext;
|
|
3996
|
+
positionTracker;
|
|
3519
3997
|
// Perp trading components (null if perp not enabled)
|
|
3520
3998
|
perpClient = null;
|
|
3521
3999
|
perpSigner = null;
|
|
@@ -3540,7 +4018,7 @@ var AgentRuntime = class {
|
|
|
3540
4018
|
*/
|
|
3541
4019
|
async initialize() {
|
|
3542
4020
|
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
3543
|
-
this.client = new
|
|
4021
|
+
this.client = new import_sdk.ExagentClient({
|
|
3544
4022
|
privateKey: this.config.privateKey,
|
|
3545
4023
|
network: this.config.network
|
|
3546
4024
|
});
|
|
@@ -3558,14 +4036,20 @@ var AgentRuntime = class {
|
|
|
3558
4036
|
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
3559
4037
|
await this.syncConfigHash();
|
|
3560
4038
|
this.strategy = await loadStrategy();
|
|
4039
|
+
const store = new FileStore();
|
|
3561
4040
|
this.strategyContext = {
|
|
3562
|
-
store
|
|
4041
|
+
store,
|
|
3563
4042
|
agentId: Number(this.config.agentId),
|
|
3564
4043
|
walletAddress: this.client.address
|
|
3565
4044
|
};
|
|
4045
|
+
this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
|
|
3566
4046
|
this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
|
|
3567
4047
|
this.riskManager = new RiskManager(this.config.trading);
|
|
3568
4048
|
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
4049
|
+
const savedRisk = this.positionTracker.getRiskState();
|
|
4050
|
+
if (savedRisk.lastResetDate) {
|
|
4051
|
+
this.riskManager.restoreState(savedRisk);
|
|
4052
|
+
}
|
|
3569
4053
|
await this.initializeVaultManager();
|
|
3570
4054
|
await this.initializePerp();
|
|
3571
4055
|
await this.initializeRelay();
|
|
@@ -3751,7 +4235,7 @@ var AgentRuntime = class {
|
|
|
3751
4235
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
3752
4236
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
3753
4237
|
const nonce = await this.client.registry.getNonce(address);
|
|
3754
|
-
const linkMessage =
|
|
4238
|
+
const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
|
|
3755
4239
|
address,
|
|
3756
4240
|
agentId,
|
|
3757
4241
|
nonce
|
|
@@ -3842,15 +4326,14 @@ var AgentRuntime = class {
|
|
|
3842
4326
|
async syncConfigHash() {
|
|
3843
4327
|
const agentId = BigInt(this.config.agentId);
|
|
3844
4328
|
const llmMeta = this.llm.getMetadata();
|
|
3845
|
-
this.configHash =
|
|
4329
|
+
this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
3846
4330
|
console.log(`Config hash: ${this.configHash}`);
|
|
3847
4331
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
3848
4332
|
if (onChainHash !== this.configHash) {
|
|
3849
4333
|
console.log("Config changed, updating on-chain...");
|
|
3850
4334
|
try {
|
|
3851
4335
|
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
3852
|
-
|
|
3853
|
-
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
4336
|
+
console.log(`Config updated on-chain`);
|
|
3854
4337
|
} catch (error) {
|
|
3855
4338
|
const message = error instanceof Error ? error.message : String(error);
|
|
3856
4339
|
if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
@@ -3881,8 +4364,7 @@ var AgentRuntime = class {
|
|
|
3881
4364
|
console.log(" ETH detected! Retrying config update...");
|
|
3882
4365
|
console.log("");
|
|
3883
4366
|
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
3884
|
-
|
|
3885
|
-
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
4367
|
+
console.log(`Config updated on-chain`);
|
|
3886
4368
|
return;
|
|
3887
4369
|
}
|
|
3888
4370
|
process.stdout.write(".");
|
|
@@ -3893,8 +4375,7 @@ var AgentRuntime = class {
|
|
|
3893
4375
|
}
|
|
3894
4376
|
}
|
|
3895
4377
|
} else {
|
|
3896
|
-
|
|
3897
|
-
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
4378
|
+
console.log("Config hash matches on-chain");
|
|
3898
4379
|
}
|
|
3899
4380
|
}
|
|
3900
4381
|
/**
|
|
@@ -4028,6 +4509,10 @@ var AgentRuntime = class {
|
|
|
4028
4509
|
}
|
|
4029
4510
|
if (updated) {
|
|
4030
4511
|
this.riskManager = new RiskManager(this.config.trading);
|
|
4512
|
+
const savedRiskState = this.positionTracker.getRiskState();
|
|
4513
|
+
if (savedRiskState.lastResetDate) {
|
|
4514
|
+
this.riskManager.restoreState(savedRiskState);
|
|
4515
|
+
}
|
|
4031
4516
|
console.log("Risk params updated via command center");
|
|
4032
4517
|
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
4033
4518
|
this.relay?.sendMessage(
|
|
@@ -4231,6 +4716,7 @@ var AgentRuntime = class {
|
|
|
4231
4716
|
mode: this.mode,
|
|
4232
4717
|
agentId: String(this.config.agentId),
|
|
4233
4718
|
wallet: this.client?.address,
|
|
4719
|
+
sdkVersion: AGENT_VERSION,
|
|
4234
4720
|
cycleCount: this.cycleCount,
|
|
4235
4721
|
lastCycleAt: this.lastCycleAt,
|
|
4236
4722
|
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
@@ -4259,7 +4745,8 @@ var AgentRuntime = class {
|
|
|
4259
4745
|
openPositions: 0,
|
|
4260
4746
|
effectiveLeverage: 0,
|
|
4261
4747
|
pendingRecords: this.perpRecorder?.pendingRetries ?? 0
|
|
4262
|
-
} : void 0
|
|
4748
|
+
} : void 0,
|
|
4749
|
+
positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0
|
|
4263
4750
|
};
|
|
4264
4751
|
if (this.perpConnected && this.perpPositions && status.perp) {
|
|
4265
4752
|
this.perpPositions.getAccountSummary().then((account) => {
|
|
@@ -4290,14 +4777,19 @@ var AgentRuntime = class {
|
|
|
4290
4777
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
4291
4778
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
4292
4779
|
this.lastPortfolioValue = marketData.portfolioValue;
|
|
4780
|
+
this.lastPrices = marketData.prices;
|
|
4293
4781
|
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4294
4782
|
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
4783
|
+
this.positionTracker.syncBalances(marketData.balances, marketData.prices);
|
|
4295
4784
|
const fundsOk = this.checkFundsLow(marketData);
|
|
4296
4785
|
if (!fundsOk) {
|
|
4297
4786
|
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
4298
4787
|
this.sendRelayStatus();
|
|
4299
4788
|
return;
|
|
4300
4789
|
}
|
|
4790
|
+
this.strategyContext.positions = this.positionTracker.getPositions();
|
|
4791
|
+
this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
|
|
4792
|
+
this.strategyContext.positionTracker = this.positionTracker;
|
|
4301
4793
|
let signals;
|
|
4302
4794
|
try {
|
|
4303
4795
|
signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
|
|
@@ -4363,13 +4855,30 @@ var AgentRuntime = class {
|
|
|
4363
4855
|
);
|
|
4364
4856
|
}
|
|
4365
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
|
+
}
|
|
4366
4873
|
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
4367
4874
|
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
4368
4875
|
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
4369
4876
|
this.riskManager.updatePnL(marketPnL);
|
|
4877
|
+
this.positionTracker.saveRiskState(this.riskManager.exportState());
|
|
4370
4878
|
if (marketPnL !== 0) {
|
|
4371
4879
|
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
4372
4880
|
}
|
|
4881
|
+
this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
|
|
4373
4882
|
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
4374
4883
|
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4375
4884
|
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
@@ -4644,192 +5153,6 @@ var AgentRuntime = class {
|
|
|
4644
5153
|
|
|
4645
5154
|
// src/cli.ts
|
|
4646
5155
|
var import_accounts6 = require("viem/accounts");
|
|
4647
|
-
|
|
4648
|
-
// src/secure-env.ts
|
|
4649
|
-
var crypto = __toESM(require("crypto"));
|
|
4650
|
-
var fs = __toESM(require("fs"));
|
|
4651
|
-
var path = __toESM(require("path"));
|
|
4652
|
-
var ALGORITHM = "aes-256-gcm";
|
|
4653
|
-
var PBKDF2_ITERATIONS = 1e5;
|
|
4654
|
-
var SALT_LENGTH = 32;
|
|
4655
|
-
var IV_LENGTH = 16;
|
|
4656
|
-
var KEY_LENGTH = 32;
|
|
4657
|
-
var SENSITIVE_PATTERNS = [
|
|
4658
|
-
/PRIVATE_KEY$/i,
|
|
4659
|
-
/_API_KEY$/i,
|
|
4660
|
-
/API_KEY$/i,
|
|
4661
|
-
/_SECRET$/i,
|
|
4662
|
-
/^OPENAI_API_KEY$/i,
|
|
4663
|
-
/^ANTHROPIC_API_KEY$/i,
|
|
4664
|
-
/^GOOGLE_AI_API_KEY$/i,
|
|
4665
|
-
/^DEEPSEEK_API_KEY$/i,
|
|
4666
|
-
/^MISTRAL_API_KEY$/i,
|
|
4667
|
-
/^GROQ_API_KEY$/i,
|
|
4668
|
-
/^TOGETHER_API_KEY$/i
|
|
4669
|
-
];
|
|
4670
|
-
function isSensitiveKey(key) {
|
|
4671
|
-
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
4672
|
-
}
|
|
4673
|
-
function deriveKey(passphrase, salt) {
|
|
4674
|
-
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
4675
|
-
}
|
|
4676
|
-
function encryptValue(value, key) {
|
|
4677
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
4678
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
4679
|
-
let encrypted = cipher.update(value, "utf8", "hex");
|
|
4680
|
-
encrypted += cipher.final("hex");
|
|
4681
|
-
const tag = cipher.getAuthTag();
|
|
4682
|
-
return {
|
|
4683
|
-
iv: iv.toString("hex"),
|
|
4684
|
-
encrypted,
|
|
4685
|
-
tag: tag.toString("hex")
|
|
4686
|
-
};
|
|
4687
|
-
}
|
|
4688
|
-
function decryptValue(encrypted, key, iv, tag) {
|
|
4689
|
-
const decipher = crypto.createDecipheriv(
|
|
4690
|
-
ALGORITHM,
|
|
4691
|
-
key,
|
|
4692
|
-
Buffer.from(iv, "hex")
|
|
4693
|
-
);
|
|
4694
|
-
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
4695
|
-
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
4696
|
-
decrypted += decipher.final("utf8");
|
|
4697
|
-
return decrypted;
|
|
4698
|
-
}
|
|
4699
|
-
function parseEnvFile(content) {
|
|
4700
|
-
const entries = [];
|
|
4701
|
-
for (const line of content.split("\n")) {
|
|
4702
|
-
const trimmed = line.trim();
|
|
4703
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
4704
|
-
const eqIndex = trimmed.indexOf("=");
|
|
4705
|
-
if (eqIndex === -1) continue;
|
|
4706
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
4707
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
4708
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4709
|
-
value = value.slice(1, -1);
|
|
4710
|
-
}
|
|
4711
|
-
if (key && value) {
|
|
4712
|
-
entries.push({ key, value });
|
|
4713
|
-
}
|
|
4714
|
-
}
|
|
4715
|
-
return entries;
|
|
4716
|
-
}
|
|
4717
|
-
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
4718
|
-
if (!fs.existsSync(envPath)) {
|
|
4719
|
-
throw new Error(`File not found: ${envPath}`);
|
|
4720
|
-
}
|
|
4721
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
4722
|
-
const entries = parseEnvFile(content);
|
|
4723
|
-
if (entries.length === 0) {
|
|
4724
|
-
throw new Error("No environment variables found in file");
|
|
4725
|
-
}
|
|
4726
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
4727
|
-
const key = deriveKey(passphrase, salt);
|
|
4728
|
-
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
4729
|
-
if (isSensitiveKey(envKey)) {
|
|
4730
|
-
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
4731
|
-
return {
|
|
4732
|
-
key: envKey,
|
|
4733
|
-
value: encrypted,
|
|
4734
|
-
encrypted: true,
|
|
4735
|
-
iv,
|
|
4736
|
-
tag
|
|
4737
|
-
};
|
|
4738
|
-
}
|
|
4739
|
-
return {
|
|
4740
|
-
key: envKey,
|
|
4741
|
-
value,
|
|
4742
|
-
encrypted: false
|
|
4743
|
-
};
|
|
4744
|
-
});
|
|
4745
|
-
const encryptedEnv = {
|
|
4746
|
-
version: 1,
|
|
4747
|
-
salt: salt.toString("hex"),
|
|
4748
|
-
entries: encryptedEntries
|
|
4749
|
-
};
|
|
4750
|
-
const encPath = envPath + ".enc";
|
|
4751
|
-
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
4752
|
-
if (deleteOriginal) {
|
|
4753
|
-
fs.unlinkSync(envPath);
|
|
4754
|
-
}
|
|
4755
|
-
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
4756
|
-
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
4757
|
-
console.log(
|
|
4758
|
-
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
4759
|
-
);
|
|
4760
|
-
return encPath;
|
|
4761
|
-
}
|
|
4762
|
-
function decryptEnvFile(encPath, passphrase) {
|
|
4763
|
-
if (!fs.existsSync(encPath)) {
|
|
4764
|
-
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
4765
|
-
}
|
|
4766
|
-
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
4767
|
-
if (content.version !== 1) {
|
|
4768
|
-
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
4769
|
-
}
|
|
4770
|
-
const salt = Buffer.from(content.salt, "hex");
|
|
4771
|
-
const key = deriveKey(passphrase, salt);
|
|
4772
|
-
const result = {};
|
|
4773
|
-
for (const entry of content.entries) {
|
|
4774
|
-
if (entry.encrypted) {
|
|
4775
|
-
if (!entry.iv || !entry.tag) {
|
|
4776
|
-
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
4777
|
-
}
|
|
4778
|
-
try {
|
|
4779
|
-
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
4780
|
-
} catch {
|
|
4781
|
-
throw new Error(
|
|
4782
|
-
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
4783
|
-
);
|
|
4784
|
-
}
|
|
4785
|
-
} else {
|
|
4786
|
-
result[entry.key] = entry.value;
|
|
4787
|
-
}
|
|
4788
|
-
}
|
|
4789
|
-
return result;
|
|
4790
|
-
}
|
|
4791
|
-
function loadSecureEnv(basePath, passphrase) {
|
|
4792
|
-
const encPath = path.join(basePath, ".env.enc");
|
|
4793
|
-
const envPath = path.join(basePath, ".env");
|
|
4794
|
-
if (fs.existsSync(encPath)) {
|
|
4795
|
-
if (!passphrase) {
|
|
4796
|
-
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
4797
|
-
}
|
|
4798
|
-
if (!passphrase) {
|
|
4799
|
-
console.warn("");
|
|
4800
|
-
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
4801
|
-
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
4802
|
-
console.warn(" pass --passphrase when running the agent.");
|
|
4803
|
-
console.warn(" Falling back to plaintext .env file.");
|
|
4804
|
-
console.warn("");
|
|
4805
|
-
} else {
|
|
4806
|
-
const vars = decryptEnvFile(encPath, passphrase);
|
|
4807
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
4808
|
-
process.env[key] = value;
|
|
4809
|
-
}
|
|
4810
|
-
return true;
|
|
4811
|
-
}
|
|
4812
|
-
}
|
|
4813
|
-
if (fs.existsSync(envPath)) {
|
|
4814
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
4815
|
-
const entries = parseEnvFile(content);
|
|
4816
|
-
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
4817
|
-
if (sensitiveKeys.length > 0) {
|
|
4818
|
-
console.warn("");
|
|
4819
|
-
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
4820
|
-
for (const key of sensitiveKeys) {
|
|
4821
|
-
console.warn(` - ${key}`);
|
|
4822
|
-
}
|
|
4823
|
-
console.warn("");
|
|
4824
|
-
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
4825
|
-
console.warn("");
|
|
4826
|
-
}
|
|
4827
|
-
return false;
|
|
4828
|
-
}
|
|
4829
|
-
return false;
|
|
4830
|
-
}
|
|
4831
|
-
|
|
4832
|
-
// src/cli.ts
|
|
4833
5156
|
(0, import_dotenv2.config)();
|
|
4834
5157
|
var program = new import_commander.Command();
|
|
4835
5158
|
program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");
|