@exagent/agent 0.1.31 → 0.1.33

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/cli.js CHANGED
@@ -32,13 +32,296 @@ var path2 = __toESM(require("path"));
32
32
 
33
33
  // src/runtime.ts
34
34
  var import_sdk = require("@exagent/sdk");
35
- var import_viem6 = require("viem");
35
+ var import_viem7 = require("viem");
36
36
  var import_chains4 = require("viem/chains");
37
37
  var import_accounts5 = require("viem/accounts");
38
38
 
39
39
  // src/trading/market.ts
40
+ var import_viem2 = require("viem");
41
+
42
+ // src/trading/token-resolver.ts
40
43
  var import_viem = require("viem");
44
+ var STORE_KEY = "__token_metadata";
45
+ var UNRESOLVABLE_KEY = "__unresolvable_tokens";
46
+ var HARDCODED = {
47
+ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": { decimals: 18, symbol: "ETH" },
48
+ // Core (0)
49
+ "0x4200000000000000000000000000000000000006": { decimals: 18, symbol: "WETH" },
50
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { decimals: 6, symbol: "USDC" },
51
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": { decimals: 18, symbol: "cbETH" },
52
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": { decimals: 8, symbol: "cbBTC" },
53
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": { decimals: 18, symbol: "wstETH" },
54
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": { decimals: 18, symbol: "DAI" },
55
+ "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2": { decimals: 6, symbol: "USDT" },
56
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": { decimals: 6, symbol: "USDbC" },
57
+ "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42": { decimals: 6, symbol: "EURC" },
58
+ "0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c": { decimals: 18, symbol: "rETH" },
59
+ "0x0555e30da8f98308edb960aa94c0db47230d2b9c": { decimals: 8, symbol: "WBTC" },
60
+ // Established (1)
61
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": { decimals: 18, symbol: "AERO" },
62
+ "0x04c0599ae5a44757c0af6f9ec3b93da8976c150a": { decimals: 18, symbol: "weETH" },
63
+ "0x2416092f143378750bb29b79ed961ab195cceea5": { decimals: 18, symbol: "ezETH" },
64
+ "0xa88594d404727625a9437c3f886c7643872296ae": { decimals: 18, symbol: "WELL" },
65
+ "0xbaa5cc21fd487b8fcc2f632f3f4e8d37262a0842": { decimals: 18, symbol: "MORPHO" },
66
+ "0x88fb150bdc53a65fe94dea0c9ba0a6daf8c6e196": { decimals: 18, symbol: "LINK" },
67
+ "0xc3de830ea07524a0761646a6a4e4be0e114a3c83": { decimals: 18, symbol: "UNI" },
68
+ "0x63706e401c06ac8513145b7687a14804d17f814b": { decimals: 18, symbol: "AAVE" },
69
+ "0x9e1028f5f1d5ede59748ffcee5532509976840e0": { decimals: 18, symbol: "COMP" },
70
+ "0x4158734d47fc9692176b5085e0f52ee0da5d47f1": { decimals: 18, symbol: "BAL" },
71
+ "0x8ee73c484a26e0a5df2ee2a4960b789967dd0415": { decimals: 18, symbol: "CRV" },
72
+ "0x22e6966b799c4d5b13be962e1d117b56327fda66": { decimals: 18, symbol: "SNX" },
73
+ // Derivatives (2)
74
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": { decimals: 18, symbol: "VIRTUAL" },
75
+ "0x4f9fd6be4a90f2620860d680c0d4d5fb53d1a825": { decimals: 18, symbol: "AIXBT" },
76
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": { decimals: 18, symbol: "DEGEN" },
77
+ "0x0578d8a44db98b23bf096a382e016e29a5ce0ffe": { decimals: 18, symbol: "HIGHER" },
78
+ "0x1bc0c42215582d5a085795f4badbaac3ff36d1bcb": { decimals: 18, symbol: "CLANKER" },
79
+ // Emerging (3)
80
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": { decimals: 18, symbol: "BRETT" },
81
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": { decimals: 18, symbol: "TOSHI" },
82
+ "0x6921b130d297cc43754afba22e5eac0fbf8db75b": { decimals: 18, symbol: "DOGINME" },
83
+ "0xb1a03eda10342529bbf8eb700a06c60441fef25d": { decimals: 18, symbol: "MIGGLES" },
84
+ "0x7f12d13b34f5f4f0a9449c16bcd42f0da47af200": { decimals: 9, symbol: "NORMIE" },
85
+ "0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8": { decimals: 18, symbol: "BALD" },
86
+ "0x768be13e1680b5ebe0024c42c896e3db59ec0149": { decimals: 9, symbol: "SKI" }
87
+ };
88
+ var TokenResolver = class {
89
+ client;
90
+ store;
91
+ cache = {};
92
+ unresolvable = /* @__PURE__ */ new Set();
93
+ constructor(client, store) {
94
+ this.client = client;
95
+ this.store = store || null;
96
+ for (const [addr, info] of Object.entries(HARDCODED)) {
97
+ this.cache[addr] = {
98
+ decimals: info.decimals,
99
+ symbol: info.symbol,
100
+ source: "hardcoded",
101
+ resolvedAt: 0
102
+ };
103
+ }
104
+ if (this.store) {
105
+ const saved = this.store.get(STORE_KEY);
106
+ if (saved) {
107
+ for (const [addr, meta] of Object.entries(saved)) {
108
+ const key = addr.toLowerCase();
109
+ if (!this.cache[key]) {
110
+ this.cache[key] = meta;
111
+ }
112
+ }
113
+ }
114
+ const savedUnresolvable = this.store.get(UNRESOLVABLE_KEY);
115
+ if (savedUnresolvable) {
116
+ for (const addr of savedUnresolvable) {
117
+ this.unresolvable.add(addr.toLowerCase());
118
+ }
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Get decimals for a token (synchronous).
124
+ * Returns undefined if the token has never been resolved and is not hardcoded.
125
+ * Call resolve() first for unknown tokens.
126
+ */
127
+ getDecimals(address) {
128
+ const meta = this.cache[address.toLowerCase()];
129
+ return meta?.decimals;
130
+ }
131
+ /**
132
+ * Get symbol for a token (synchronous).
133
+ */
134
+ getSymbol(address) {
135
+ return this.cache[address.toLowerCase()]?.symbol;
136
+ }
137
+ /**
138
+ * Set symbol for a token (used when DexScreener returns symbol data).
139
+ */
140
+ setSymbol(address, symbol) {
141
+ const key = address.toLowerCase();
142
+ if (this.cache[key]) {
143
+ if (!this.cache[key].symbol) {
144
+ this.cache[key].symbol = symbol;
145
+ this.persist();
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Check if a token has been marked as unresolvable by all tiers.
151
+ */
152
+ isUnresolvable(address) {
153
+ return this.unresolvable.has(address.toLowerCase());
154
+ }
155
+ /**
156
+ * Batch-resolve metadata for a list of token addresses.
157
+ *
158
+ * Skips already-resolved tokens. For unknown tokens:
159
+ * 1. Multicall3 on-chain batch (decimals + symbol)
160
+ * 2. Blockscout API for any multicall failures
161
+ * 3. Mark remaining as unresolvable
162
+ */
163
+ async resolve(addresses) {
164
+ const unknown = addresses.filter((addr) => {
165
+ const key = addr.toLowerCase();
166
+ return !this.cache[key] && !this.unresolvable.has(key);
167
+ });
168
+ if (unknown.length === 0) return;
169
+ const unique = [...new Set(unknown.map((a) => a.toLowerCase()))];
170
+ const stillUnresolved = await this.resolveViaMulticall(unique);
171
+ if (stillUnresolved.length > 0) {
172
+ const afterBlockscout = await this.resolveViaBlockscout(stillUnresolved);
173
+ if (afterBlockscout.length > 0) {
174
+ for (const addr of afterBlockscout) {
175
+ this.unresolvable.add(addr);
176
+ }
177
+ console.warn(
178
+ `TokenResolver: ${afterBlockscout.length} token(s) unresolvable after all tiers \u2014 skipping from portfolio: ${afterBlockscout.map((a) => a.slice(0, 10) + "...").join(", ")}`
179
+ );
180
+ this.persistUnresolvable();
181
+ }
182
+ }
183
+ }
184
+ /**
185
+ * Tier 2: Batch-fetch decimals + symbol via Multicall3.
186
+ * Returns addresses that failed (for next tier).
187
+ */
188
+ async resolveViaMulticall(addresses) {
189
+ const failed = [];
190
+ try {
191
+ const contracts = addresses.flatMap((addr) => [
192
+ {
193
+ address: addr,
194
+ abi: import_viem.erc20Abi,
195
+ functionName: "decimals"
196
+ },
197
+ {
198
+ address: addr,
199
+ abi: import_viem.erc20Abi,
200
+ functionName: "symbol"
201
+ }
202
+ ]);
203
+ const BATCH_SIZE = 100;
204
+ const allResults = [];
205
+ for (let i = 0; i < contracts.length; i += BATCH_SIZE) {
206
+ const batch = contracts.slice(i, i + BATCH_SIZE);
207
+ try {
208
+ const results = await this.client.multicall({
209
+ contracts: batch,
210
+ allowFailure: true
211
+ });
212
+ allResults.push(...results);
213
+ } catch {
214
+ for (let j = 0; j < batch.length; j += 2) {
215
+ allResults.push({ status: "failure" }, { status: "failure" });
216
+ }
217
+ }
218
+ }
219
+ for (let i = 0; i < addresses.length; i++) {
220
+ const decimalsResult = allResults[i * 2];
221
+ const symbolResult = allResults[i * 2 + 1];
222
+ const addr = addresses[i];
223
+ if (decimalsResult?.status === "success" && decimalsResult.result !== void 0) {
224
+ const decimals = Number(decimalsResult.result);
225
+ const symbol = symbolResult?.status === "success" && symbolResult.result ? String(symbolResult.result) : void 0;
226
+ this.cache[addr] = {
227
+ decimals,
228
+ symbol,
229
+ source: "onchain",
230
+ resolvedAt: Date.now()
231
+ };
232
+ if (symbol) {
233
+ console.log(`TokenResolver: ${symbol} (${addr.slice(0, 10)}...) \u2192 ${decimals} decimals [onchain]`);
234
+ } else {
235
+ console.log(`TokenResolver: ${addr.slice(0, 10)}... \u2192 ${decimals} decimals [onchain]`);
236
+ }
237
+ } else {
238
+ failed.push(addr);
239
+ }
240
+ }
241
+ this.persist();
242
+ } catch (error) {
243
+ console.warn("TokenResolver: Multicall3 failed entirely, trying Blockscout:", error instanceof Error ? error.message : error);
244
+ return addresses;
245
+ }
246
+ return failed;
247
+ }
248
+ /**
249
+ * Tier 3: Fetch token metadata from Blockscout (Base's block explorer API).
250
+ * Free, no API key, near-universal coverage for any token indexed by the explorer.
251
+ * Returns addresses that failed (truly unresolvable).
252
+ */
253
+ async resolveViaBlockscout(addresses) {
254
+ const failed = [];
255
+ const CONCURRENCY = 5;
256
+ for (let i = 0; i < addresses.length; i += CONCURRENCY) {
257
+ const batch = addresses.slice(i, i + CONCURRENCY);
258
+ const results = await Promise.allSettled(
259
+ batch.map(async (addr) => {
260
+ const response = await fetch(
261
+ `https://base.blockscout.com/api/v2/tokens/${addr}`,
262
+ { signal: AbortSignal.timeout(8e3) }
263
+ );
264
+ if (!response.ok) return null;
265
+ const data = await response.json();
266
+ if (data.decimals === null || data.decimals === void 0) return null;
267
+ const decimals = parseInt(data.decimals, 10);
268
+ if (isNaN(decimals)) return null;
269
+ return {
270
+ decimals,
271
+ symbol: data.symbol || data.name || void 0
272
+ };
273
+ })
274
+ );
275
+ for (let j = 0; j < batch.length; j++) {
276
+ const addr = batch[j];
277
+ const result = results[j];
278
+ if (result.status === "fulfilled" && result.value) {
279
+ this.cache[addr] = {
280
+ decimals: result.value.decimals,
281
+ symbol: result.value.symbol,
282
+ source: "blockscout",
283
+ resolvedAt: Date.now()
284
+ };
285
+ const sym = result.value.symbol || addr.slice(0, 10) + "...";
286
+ console.log(`TokenResolver: ${sym} \u2192 ${result.value.decimals} decimals [blockscout]`);
287
+ } else {
288
+ failed.push(addr);
289
+ }
290
+ }
291
+ }
292
+ if (failed.length === 0 || addresses.length > failed.length) {
293
+ this.persist();
294
+ }
295
+ return failed;
296
+ }
297
+ /**
298
+ * Persist resolved metadata to FileStore.
299
+ */
300
+ persist() {
301
+ if (!this.store) return;
302
+ const toSave = {};
303
+ for (const [addr, meta] of Object.entries(this.cache)) {
304
+ if (meta.source !== "hardcoded") {
305
+ toSave[addr] = meta;
306
+ }
307
+ }
308
+ this.store.set(STORE_KEY, toSave);
309
+ }
310
+ /**
311
+ * Persist unresolvable token list to FileStore.
312
+ */
313
+ persistUnresolvable() {
314
+ if (!this.store) return;
315
+ this.store.set(UNRESOLVABLE_KEY, [...this.unresolvable]);
316
+ }
317
+ };
318
+
319
+ // src/trading/market.ts
41
320
  var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
321
+ var _globalResolver = null;
322
+ function setGlobalResolver(resolver) {
323
+ _globalResolver = resolver;
324
+ }
42
325
  var TOKEN_DECIMALS = {
43
326
  [NATIVE_ETH.toLowerCase()]: 18,
44
327
  // Native ETH
@@ -117,54 +400,19 @@ var TOKEN_DECIMALS = {
117
400
  "0x768be13e1680b5ebe0024c42c896e3db59ec0149": 9
118
401
  // SKI
119
402
  };
120
- var decimalsCache = {};
121
- var symbolCache = {};
122
403
  function getTokenDecimals(address) {
123
404
  const key = address.toLowerCase();
405
+ if (_globalResolver) {
406
+ const d = _globalResolver.getDecimals(key);
407
+ if (d !== void 0) return d;
408
+ if (_globalResolver.isUnresolvable(key)) return 18;
409
+ }
124
410
  const known = TOKEN_DECIMALS[key];
125
411
  if (known !== void 0) return known;
126
- const cached = decimalsCache[key];
127
- if (cached !== void 0) return cached;
128
- console.warn(`Unknown token decimals for ${address}, defaulting to 18. Call fetchTokenDecimals() first for accuracy.`);
129
412
  return 18;
130
413
  }
131
414
  function getTokenSymbol(address) {
132
- return symbolCache[address.toLowerCase()];
133
- }
134
- async function fetchTokenDecimals(client, address) {
135
- const key = address.toLowerCase();
136
- const known = TOKEN_DECIMALS[key];
137
- if (known !== void 0) return known;
138
- const cached = decimalsCache[key];
139
- if (cached !== void 0) return cached;
140
- for (let attempt = 0; attempt < 2; attempt++) {
141
- try {
142
- const [decimals, symbol] = await Promise.all([
143
- client.readContract({
144
- address,
145
- abi: import_viem.erc20Abi,
146
- functionName: "decimals"
147
- }),
148
- client.readContract({
149
- address,
150
- abi: import_viem.erc20Abi,
151
- functionName: "symbol"
152
- }).catch(() => void 0)
153
- // symbol is optional — some tokens don't have it
154
- ]);
155
- const result = Number(decimals);
156
- decimalsCache[key] = result;
157
- if (symbol) symbolCache[key] = String(symbol);
158
- return result;
159
- } catch {
160
- if (attempt === 0) {
161
- await new Promise((r) => setTimeout(r, 2e3));
162
- }
163
- }
164
- }
165
- console.warn(`Failed to fetch decimals for ${address} after 2 attempts, defaulting to 18`);
166
- decimalsCache[key] = 18;
167
- return 18;
415
+ return _globalResolver?.getSymbol(address.toLowerCase());
168
416
  }
169
417
  var TOKEN_TO_COINGECKO = {
170
418
  [NATIVE_ETH.toLowerCase()]: "ethereum",
@@ -248,24 +496,31 @@ var PRICE_STALENESS_MS = 6e4;
248
496
  var MarketDataService = class {
249
497
  rpcUrl;
250
498
  client;
499
+ resolver;
251
500
  /** Cached prices from last fetch */
252
501
  cachedPrices = {};
253
502
  /** Timestamp of last successful price fetch */
254
503
  lastPriceFetchAt = 0;
255
- constructor(rpcUrl) {
256
- this.rpcUrl = rpcUrl;
257
- this.client = (0, import_viem.createPublicClient)({
258
- transport: (0, import_viem.http)(rpcUrl, { timeout: 6e4 })
259
- });
260
- }
261
504
  /** Cached volume data */
262
505
  cachedVolume24h = {};
263
506
  /** Cached price change data */
264
507
  cachedPriceChange24h = {};
508
+ constructor(rpcUrl, store) {
509
+ this.rpcUrl = rpcUrl;
510
+ this.client = (0, import_viem2.createPublicClient)({
511
+ transport: (0, import_viem2.http)(rpcUrl, { timeout: 6e4 })
512
+ });
513
+ this.resolver = new TokenResolver(this.client, store);
514
+ }
515
+ /** Get the underlying TokenResolver for direct access */
516
+ getResolver() {
517
+ return this.resolver;
518
+ }
265
519
  /**
266
520
  * Fetch current market data for the agent
267
521
  */
268
522
  async fetchMarketData(walletAddress, tokenAddresses) {
523
+ await this.resolver.resolve(tokenAddresses);
269
524
  const prices = await this.fetchPrices(tokenAddresses);
270
525
  const balances = await this.fetchBalances(walletAddress, tokenAddresses);
271
526
  const portfolioValue = this.calculatePortfolioValue(balances, prices);
@@ -402,14 +657,14 @@ var MarketDataService = class {
402
657
  const liq = pair.liquidity?.usd || 0;
403
658
  if (price > 0 && (!bestPrices[addr] || liq > bestPrices[addr].liquidity)) {
404
659
  bestPrices[addr] = { price, liquidity: liq };
405
- if (pair.baseToken.symbol && !symbolCache[addr]) {
406
- symbolCache[addr] = pair.baseToken.symbol;
660
+ if (pair.baseToken.symbol) {
661
+ this.resolver.setSymbol(addr, pair.baseToken.symbol);
407
662
  }
408
663
  }
409
664
  }
410
665
  for (const [addr, { price }] of Object.entries(bestPrices)) {
411
666
  prices[addr] = price;
412
- const sym = symbolCache[addr] || addr.slice(0, 10);
667
+ const sym = this.resolver.getSymbol(addr) || addr.slice(0, 10);
413
668
  console.log(`DexScreener price for ${sym}: $${price}`);
414
669
  }
415
670
  }
@@ -426,7 +681,7 @@ var MarketDataService = class {
426
681
  }
427
682
  for (const addr of tokenAddresses) {
428
683
  if (!prices[addr.toLowerCase()]) {
429
- const sym = symbolCache[addr.toLowerCase()];
684
+ const sym = this.resolver.getSymbol(addr.toLowerCase());
430
685
  console.warn(`No price available for ${sym ? `${sym} (${addr})` : addr} \u2014 CoinGecko, DeFi Llama, and DexScreener all returned nothing`);
431
686
  prices[addr.toLowerCase()] = 0;
432
687
  }
@@ -434,36 +689,35 @@ var MarketDataService = class {
434
689
  return prices;
435
690
  }
436
691
  /**
437
- * Fetch real on-chain balances: native ETH + ERC-20 tokens
692
+ * Fetch real on-chain balances: native ETH + ERC-20 tokens.
693
+ * Uses Multicall3 to batch all balanceOf calls into a single RPC request.
438
694
  */
439
695
  async fetchBalances(walletAddress, tokenAddresses) {
440
696
  const balances = {};
441
697
  const wallet = walletAddress;
442
698
  try {
443
- const unknownTokens = tokenAddresses.filter(
444
- (addr) => TOKEN_DECIMALS[addr.toLowerCase()] === void 0 && !decimalsCache[addr.toLowerCase()]
445
- );
446
- if (unknownTokens.length > 0) {
447
- await Promise.all(unknownTokens.map((addr) => fetchTokenDecimals(this.client, addr)));
448
- }
449
699
  const nativeBalance = await this.client.getBalance({ address: wallet });
450
700
  balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
451
- const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
701
+ if (tokenAddresses.length > 0) {
452
702
  try {
453
- const balance = await this.client.readContract({
454
- address: tokenAddress,
455
- abi: import_viem.erc20Abi,
456
- functionName: "balanceOf",
457
- args: [wallet]
703
+ const results = await this.client.multicall({
704
+ contracts: tokenAddresses.map((addr) => ({
705
+ address: addr,
706
+ abi: import_viem2.erc20Abi,
707
+ functionName: "balanceOf",
708
+ args: [wallet]
709
+ })),
710
+ allowFailure: true
458
711
  });
459
- return { address: tokenAddress.toLowerCase(), balance };
460
- } catch (error) {
461
- return { address: tokenAddress.toLowerCase(), balance: 0n };
712
+ for (let i = 0; i < tokenAddresses.length; i++) {
713
+ const addr = tokenAddresses[i].toLowerCase();
714
+ const r = results[i];
715
+ balances[addr] = r.status === "success" ? r.result : 0n;
716
+ }
717
+ } catch {
718
+ console.warn("Multicall3 balanceOf failed, falling back to individual queries");
719
+ await this.fetchBalancesIndividual(wallet, tokenAddresses, balances);
462
720
  }
463
- });
464
- const results = await Promise.all(erc20Promises);
465
- for (const { address, balance } of results) {
466
- balances[address] = balance;
467
721
  }
468
722
  } catch (error) {
469
723
  console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
@@ -475,13 +729,42 @@ var MarketDataService = class {
475
729
  return balances;
476
730
  }
477
731
  /**
478
- * Calculate total portfolio value in USD
732
+ * Fallback: fetch ERC-20 balances individually (if multicall fails).
733
+ */
734
+ async fetchBalancesIndividual(wallet, tokenAddresses, balances) {
735
+ const promises = tokenAddresses.map(async (tokenAddress) => {
736
+ try {
737
+ const balance = await this.client.readContract({
738
+ address: tokenAddress,
739
+ abi: import_viem2.erc20Abi,
740
+ functionName: "balanceOf",
741
+ args: [wallet]
742
+ });
743
+ return { address: tokenAddress.toLowerCase(), balance };
744
+ } catch {
745
+ return { address: tokenAddress.toLowerCase(), balance: 0n };
746
+ }
747
+ });
748
+ const results = await Promise.all(promises);
749
+ for (const { address, balance } of results) {
750
+ balances[address] = balance;
751
+ }
752
+ }
753
+ /**
754
+ * Calculate total portfolio value in USD.
755
+ * Skips tokens with unresolvable decimals to avoid corrupted calculations.
479
756
  */
480
757
  calculatePortfolioValue(balances, prices) {
481
758
  let total = 0;
482
759
  for (const [address, balance] of Object.entries(balances)) {
483
760
  const price = prices[address.toLowerCase()] || 0;
484
- const decimals = getTokenDecimals(address);
761
+ if (price === 0) continue;
762
+ const key = address.toLowerCase();
763
+ const resolverDecimals = _globalResolver?.getDecimals(key);
764
+ if (resolverDecimals === void 0 && _globalResolver?.isUnresolvable(key)) {
765
+ continue;
766
+ }
767
+ const decimals = resolverDecimals ?? getTokenDecimals(address);
485
768
  const amount = Number(balance) / Math.pow(10, decimals);
486
769
  total += amount * price;
487
770
  }
@@ -2166,7 +2449,7 @@ var FileStore = class {
2166
2449
  };
2167
2450
 
2168
2451
  // src/vault/manager.ts
2169
- var import_viem2 = require("viem");
2452
+ var import_viem3 = require("viem");
2170
2453
  var import_accounts = require("viem/accounts");
2171
2454
  var import_chains = require("viem/chains");
2172
2455
  var ADDRESSES = {
@@ -2257,12 +2540,12 @@ var VaultManager = class {
2257
2540
  this.account = (0, import_accounts.privateKeyToAccount)(config.walletKey);
2258
2541
  this.chain = import_chains.base;
2259
2542
  const rpcUrl = getRpcUrl();
2260
- const transport = (0, import_viem2.http)(rpcUrl, { timeout: 6e4 });
2261
- this.publicClient = (0, import_viem2.createPublicClient)({
2543
+ const transport = (0, import_viem3.http)(rpcUrl, { timeout: 6e4 });
2544
+ this.publicClient = (0, import_viem3.createPublicClient)({
2262
2545
  chain: this.chain,
2263
2546
  transport
2264
2547
  });
2265
- this.walletClient = (0, import_viem2.createWalletClient)({
2548
+ this.walletClient = (0, import_viem3.createWalletClient)({
2266
2549
  account: this.account,
2267
2550
  chain: this.chain,
2268
2551
  transport
@@ -2648,7 +2931,7 @@ var HyperliquidClient = class {
2648
2931
  };
2649
2932
 
2650
2933
  // src/perp/signer.ts
2651
- var import_viem3 = require("viem");
2934
+ var import_viem4 = require("viem");
2652
2935
  var HYPERLIQUID_DOMAIN = {
2653
2936
  name: "HyperliquidSignTransaction",
2654
2937
  version: "1",
@@ -2722,7 +3005,7 @@ function fillHashToBytes32(fillHash) {
2722
3005
  if (fillHash.startsWith("0x") && fillHash.length === 66) {
2723
3006
  return fillHash;
2724
3007
  }
2725
- return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["string"], [fillHash]));
3008
+ return (0, import_viem4.keccak256)((0, import_viem4.encodePacked)(["string"], [fillHash]));
2726
3009
  }
2727
3010
 
2728
3011
  // src/perp/orders.ts
@@ -3324,7 +3607,7 @@ var HyperliquidWebSocket = class {
3324
3607
  };
3325
3608
 
3326
3609
  // src/perp/recorder.ts
3327
- var import_viem4 = require("viem");
3610
+ var import_viem5 = require("viem");
3328
3611
  var import_chains2 = require("viem/chains");
3329
3612
  var import_accounts2 = require("viem/accounts");
3330
3613
  var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
@@ -3367,12 +3650,12 @@ var PerpTradeRecorder = class {
3367
3650
  this.configHash = opts.configHash;
3368
3651
  this.account = (0, import_accounts2.privateKeyToAccount)(opts.privateKey);
3369
3652
  const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3370
- const transport = (0, import_viem4.http)(rpcUrl, { timeout: 6e4 });
3371
- this.publicClient = (0, import_viem4.createPublicClient)({
3653
+ const transport = (0, import_viem5.http)(rpcUrl, { timeout: 6e4 });
3654
+ this.publicClient = (0, import_viem5.createPublicClient)({
3372
3655
  chain: import_chains2.base,
3373
3656
  transport
3374
3657
  });
3375
- this.walletClient = (0, import_viem4.createWalletClient)({
3658
+ this.walletClient = (0, import_viem5.createWalletClient)({
3376
3659
  chain: import_chains2.base,
3377
3660
  transport,
3378
3661
  account: this.account
@@ -3680,23 +3963,23 @@ var PerpOnboarding = class {
3680
3963
  };
3681
3964
 
3682
3965
  // src/perp/funding.ts
3683
- var import_viem5 = require("viem");
3966
+ var import_viem6 = require("viem");
3684
3967
  var import_chains3 = require("viem/chains");
3685
3968
  var import_accounts3 = require("viem/accounts");
3686
- var ERC20_ABI = (0, import_viem5.parseAbi)([
3969
+ var ERC20_ABI = (0, import_viem6.parseAbi)([
3687
3970
  "function approve(address spender, uint256 amount) external returns (bool)",
3688
3971
  "function balanceOf(address account) external view returns (uint256)",
3689
3972
  "function allowance(address owner, address spender) external view returns (uint256)"
3690
3973
  ]);
3691
- var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
3974
+ var TOKEN_MESSENGER_V2_ABI = (0, import_viem6.parseAbi)([
3692
3975
  "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3693
3976
  "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3694
3977
  ]);
3695
- var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
3978
+ var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem6.parseAbi)([
3696
3979
  "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3697
3980
  "event MessageSent(bytes message)"
3698
3981
  ]);
3699
- var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
3982
+ var CORE_DEPOSIT_WALLET_ABI = (0, import_viem6.parseAbi)([
3700
3983
  "function deposit(uint256 amount, uint32 destinationDex) external"
3701
3984
  ]);
3702
3985
 
@@ -3885,7 +4168,7 @@ function loadSecureEnv(basePath, passphrase) {
3885
4168
  }
3886
4169
 
3887
4170
  // src/index.ts
3888
- var AGENT_VERSION = "0.1.31";
4171
+ var AGENT_VERSION = "0.1.32";
3889
4172
 
3890
4173
  // src/relay.ts
3891
4174
  var RelayClient = class {
@@ -4237,7 +4520,8 @@ var AgentRuntime = class {
4237
4520
  this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
4238
4521
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
4239
4522
  this.riskManager = new RiskManager(this.config.trading);
4240
- this.marketData = new MarketDataService(this.getRpcUrl());
4523
+ this.marketData = new MarketDataService(this.getRpcUrl(), store);
4524
+ setGlobalResolver(this.marketData.getResolver());
4241
4525
  const savedRisk = this.positionTracker.getRiskState();
4242
4526
  if (savedRisk.lastResetDate) {
4243
4527
  this.riskManager.restoreState(savedRisk);
@@ -4334,9 +4618,9 @@ var AgentRuntime = class {
4334
4618
  this.perpClient = new HyperliquidClient(config);
4335
4619
  const perpKey = perpConfig.perpRelayerKey || this.config.privateKey;
4336
4620
  const account = (0, import_accounts5.privateKeyToAccount)(perpKey);
4337
- const walletClient = (0, import_viem6.createWalletClient)({
4621
+ const walletClient = (0, import_viem7.createWalletClient)({
4338
4622
  chain: { id: 42161, name: "Arbitrum", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } } },
4339
- transport: (0, import_viem6.http)("https://arb1.arbitrum.io/rpc", { timeout: 6e4 }),
4623
+ transport: (0, import_viem7.http)("https://arb1.arbitrum.io/rpc", { timeout: 6e4 }),
4340
4624
  account
4341
4625
  });
4342
4626
  this.perpSigner = new HyperliquidSigner(walletClient);
@@ -4576,9 +4860,9 @@ var AgentRuntime = class {
4576
4860
  const message = error instanceof Error ? error.message : String(error);
4577
4861
  if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
4578
4862
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
4579
- const publicClientInstance = (0, import_viem6.createPublicClient)({
4863
+ const publicClientInstance = (0, import_viem7.createPublicClient)({
4580
4864
  chain: import_chains4.base,
4581
- transport: (0, import_viem6.http)(this.getRpcUrl(), { timeout: 6e4 })
4865
+ transport: (0, import_viem7.http)(this.getRpcUrl(), { timeout: 6e4 })
4582
4866
  });
4583
4867
  console.log("");
4584
4868
  console.log("=== ETH NEEDED FOR GAS ===");
@@ -4641,6 +4925,21 @@ var AgentRuntime = class {
4641
4925
  console.log("Visit https://exagent.io to start trading from the dashboard.");
4642
4926
  console.log("");
4643
4927
  this.mode = "idle";
4928
+ try {
4929
+ const tokens = this.getTokensToTrack();
4930
+ const initData = await this.marketData.fetchMarketData(this.client.address, tokens);
4931
+ this.positionTracker.syncBalances(initData.balances, initData.prices);
4932
+ this.lastPortfolioValue = initData.portfolioValue;
4933
+ this.lastPrices = initData.prices;
4934
+ const nativeEthBal = initData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4935
+ this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4936
+ const posCount = this.positionTracker.getPositions().length;
4937
+ if (posCount > 0) {
4938
+ console.log(`Initial sync: ${posCount} position(s) rebuilt from on-chain balances`);
4939
+ }
4940
+ } catch (error) {
4941
+ console.warn("Initial balance sync failed (non-fatal):", error instanceof Error ? error.message : error);
4942
+ }
4644
4943
  this.sendRelayStatus();
4645
4944
  this.relay.sendMessage(
4646
4945
  "system",
@@ -5315,7 +5614,14 @@ var AgentRuntime = class {
5315
5614
  if (extras.length > 0) {
5316
5615
  console.log(`Auto-tracking ${extras.length} additional token(s) from position history`);
5317
5616
  }
5318
- return [...base5, ...extras];
5617
+ const resolver = this.marketData.getResolver();
5618
+ const allTokens = [...base5, ...extras];
5619
+ const filtered = allTokens.filter((t) => !resolver.isUnresolvable(t.toLowerCase()));
5620
+ const dropped = allTokens.length - filtered.length;
5621
+ if (dropped > 0) {
5622
+ console.log(`Skipping ${dropped} unresolvable token(s)`);
5623
+ }
5624
+ return filtered;
5319
5625
  }
5320
5626
  /**
5321
5627
  * Default tokens to track.