@exagent/agent 0.1.32 → 0.1.34

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/index.js CHANGED
@@ -85,13 +85,296 @@ module.exports = __toCommonJS(index_exports);
85
85
 
86
86
  // src/runtime.ts
87
87
  var import_sdk = require("@exagent/sdk");
88
- var import_viem6 = require("viem");
88
+ var import_viem7 = require("viem");
89
89
  var import_chains4 = require("viem/chains");
90
90
  var import_accounts5 = require("viem/accounts");
91
91
 
92
92
  // src/trading/market.ts
93
+ var import_viem2 = require("viem");
94
+
95
+ // src/trading/token-resolver.ts
93
96
  var import_viem = require("viem");
97
+ var STORE_KEY = "__token_metadata";
98
+ var UNRESOLVABLE_KEY = "__unresolvable_tokens";
99
+ var HARDCODED = {
100
+ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": { decimals: 18, symbol: "ETH" },
101
+ // Core (0)
102
+ "0x4200000000000000000000000000000000000006": { decimals: 18, symbol: "WETH" },
103
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { decimals: 6, symbol: "USDC" },
104
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": { decimals: 18, symbol: "cbETH" },
105
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": { decimals: 8, symbol: "cbBTC" },
106
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": { decimals: 18, symbol: "wstETH" },
107
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": { decimals: 18, symbol: "DAI" },
108
+ "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2": { decimals: 6, symbol: "USDT" },
109
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": { decimals: 6, symbol: "USDbC" },
110
+ "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42": { decimals: 6, symbol: "EURC" },
111
+ "0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c": { decimals: 18, symbol: "rETH" },
112
+ "0x0555e30da8f98308edb960aa94c0db47230d2b9c": { decimals: 8, symbol: "WBTC" },
113
+ // Established (1)
114
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": { decimals: 18, symbol: "AERO" },
115
+ "0x04c0599ae5a44757c0af6f9ec3b93da8976c150a": { decimals: 18, symbol: "weETH" },
116
+ "0x2416092f143378750bb29b79ed961ab195cceea5": { decimals: 18, symbol: "ezETH" },
117
+ "0xa88594d404727625a9437c3f886c7643872296ae": { decimals: 18, symbol: "WELL" },
118
+ "0xbaa5cc21fd487b8fcc2f632f3f4e8d37262a0842": { decimals: 18, symbol: "MORPHO" },
119
+ "0x88fb150bdc53a65fe94dea0c9ba0a6daf8c6e196": { decimals: 18, symbol: "LINK" },
120
+ "0xc3de830ea07524a0761646a6a4e4be0e114a3c83": { decimals: 18, symbol: "UNI" },
121
+ "0x63706e401c06ac8513145b7687a14804d17f814b": { decimals: 18, symbol: "AAVE" },
122
+ "0x9e1028f5f1d5ede59748ffcee5532509976840e0": { decimals: 18, symbol: "COMP" },
123
+ "0x4158734d47fc9692176b5085e0f52ee0da5d47f1": { decimals: 18, symbol: "BAL" },
124
+ "0x8ee73c484a26e0a5df2ee2a4960b789967dd0415": { decimals: 18, symbol: "CRV" },
125
+ "0x22e6966b799c4d5b13be962e1d117b56327fda66": { decimals: 18, symbol: "SNX" },
126
+ // Derivatives (2)
127
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": { decimals: 18, symbol: "VIRTUAL" },
128
+ "0x4f9fd6be4a90f2620860d680c0d4d5fb53d1a825": { decimals: 18, symbol: "AIXBT" },
129
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": { decimals: 18, symbol: "DEGEN" },
130
+ "0x0578d8a44db98b23bf096a382e016e29a5ce0ffe": { decimals: 18, symbol: "HIGHER" },
131
+ "0x1bc0c42215582d5a085795f4badbaac3ff36d1bcb": { decimals: 18, symbol: "CLANKER" },
132
+ // Emerging (3)
133
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": { decimals: 18, symbol: "BRETT" },
134
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": { decimals: 18, symbol: "TOSHI" },
135
+ "0x6921b130d297cc43754afba22e5eac0fbf8db75b": { decimals: 18, symbol: "DOGINME" },
136
+ "0xb1a03eda10342529bbf8eb700a06c60441fef25d": { decimals: 18, symbol: "MIGGLES" },
137
+ "0x7f12d13b34f5f4f0a9449c16bcd42f0da47af200": { decimals: 9, symbol: "NORMIE" },
138
+ "0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8": { decimals: 18, symbol: "BALD" },
139
+ "0x768be13e1680b5ebe0024c42c896e3db59ec0149": { decimals: 9, symbol: "SKI" }
140
+ };
141
+ var TokenResolver = class {
142
+ client;
143
+ store;
144
+ cache = {};
145
+ unresolvable = /* @__PURE__ */ new Set();
146
+ constructor(client, store) {
147
+ this.client = client;
148
+ this.store = store || null;
149
+ for (const [addr, info] of Object.entries(HARDCODED)) {
150
+ this.cache[addr] = {
151
+ decimals: info.decimals,
152
+ symbol: info.symbol,
153
+ source: "hardcoded",
154
+ resolvedAt: 0
155
+ };
156
+ }
157
+ if (this.store) {
158
+ const saved = this.store.get(STORE_KEY);
159
+ if (saved) {
160
+ for (const [addr, meta] of Object.entries(saved)) {
161
+ const key = addr.toLowerCase();
162
+ if (!this.cache[key]) {
163
+ this.cache[key] = meta;
164
+ }
165
+ }
166
+ }
167
+ const savedUnresolvable = this.store.get(UNRESOLVABLE_KEY);
168
+ if (savedUnresolvable) {
169
+ for (const addr of savedUnresolvable) {
170
+ this.unresolvable.add(addr.toLowerCase());
171
+ }
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Get decimals for a token (synchronous).
177
+ * Returns undefined if the token has never been resolved and is not hardcoded.
178
+ * Call resolve() first for unknown tokens.
179
+ */
180
+ getDecimals(address) {
181
+ const meta = this.cache[address.toLowerCase()];
182
+ return meta?.decimals;
183
+ }
184
+ /**
185
+ * Get symbol for a token (synchronous).
186
+ */
187
+ getSymbol(address) {
188
+ return this.cache[address.toLowerCase()]?.symbol;
189
+ }
190
+ /**
191
+ * Set symbol for a token (used when DexScreener returns symbol data).
192
+ */
193
+ setSymbol(address, symbol) {
194
+ const key = address.toLowerCase();
195
+ if (this.cache[key]) {
196
+ if (!this.cache[key].symbol) {
197
+ this.cache[key].symbol = symbol;
198
+ this.persist();
199
+ }
200
+ }
201
+ }
202
+ /**
203
+ * Check if a token has been marked as unresolvable by all tiers.
204
+ */
205
+ isUnresolvable(address) {
206
+ return this.unresolvable.has(address.toLowerCase());
207
+ }
208
+ /**
209
+ * Batch-resolve metadata for a list of token addresses.
210
+ *
211
+ * Skips already-resolved tokens. For unknown tokens:
212
+ * 1. Multicall3 on-chain batch (decimals + symbol)
213
+ * 2. Blockscout API for any multicall failures
214
+ * 3. Mark remaining as unresolvable
215
+ */
216
+ async resolve(addresses) {
217
+ const unknown = addresses.filter((addr) => {
218
+ const key = addr.toLowerCase();
219
+ return !this.cache[key] && !this.unresolvable.has(key);
220
+ });
221
+ if (unknown.length === 0) return;
222
+ const unique = [...new Set(unknown.map((a) => a.toLowerCase()))];
223
+ const stillUnresolved = await this.resolveViaMulticall(unique);
224
+ if (stillUnresolved.length > 0) {
225
+ const afterBlockscout = await this.resolveViaBlockscout(stillUnresolved);
226
+ if (afterBlockscout.length > 0) {
227
+ for (const addr of afterBlockscout) {
228
+ this.unresolvable.add(addr);
229
+ }
230
+ console.warn(
231
+ `TokenResolver: ${afterBlockscout.length} token(s) unresolvable after all tiers \u2014 skipping from portfolio: ${afterBlockscout.map((a) => a.slice(0, 10) + "...").join(", ")}`
232
+ );
233
+ this.persistUnresolvable();
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Tier 2: Batch-fetch decimals + symbol via Multicall3.
239
+ * Returns addresses that failed (for next tier).
240
+ */
241
+ async resolveViaMulticall(addresses) {
242
+ const failed = [];
243
+ try {
244
+ const contracts = addresses.flatMap((addr) => [
245
+ {
246
+ address: addr,
247
+ abi: import_viem.erc20Abi,
248
+ functionName: "decimals"
249
+ },
250
+ {
251
+ address: addr,
252
+ abi: import_viem.erc20Abi,
253
+ functionName: "symbol"
254
+ }
255
+ ]);
256
+ const BATCH_SIZE = 100;
257
+ const allResults = [];
258
+ for (let i = 0; i < contracts.length; i += BATCH_SIZE) {
259
+ const batch = contracts.slice(i, i + BATCH_SIZE);
260
+ try {
261
+ const results = await this.client.multicall({
262
+ contracts: batch,
263
+ allowFailure: true
264
+ });
265
+ allResults.push(...results);
266
+ } catch {
267
+ for (let j = 0; j < batch.length; j += 2) {
268
+ allResults.push({ status: "failure" }, { status: "failure" });
269
+ }
270
+ }
271
+ }
272
+ for (let i = 0; i < addresses.length; i++) {
273
+ const decimalsResult = allResults[i * 2];
274
+ const symbolResult = allResults[i * 2 + 1];
275
+ const addr = addresses[i];
276
+ if (decimalsResult?.status === "success" && decimalsResult.result !== void 0) {
277
+ const decimals = Number(decimalsResult.result);
278
+ const symbol = symbolResult?.status === "success" && symbolResult.result ? String(symbolResult.result) : void 0;
279
+ this.cache[addr] = {
280
+ decimals,
281
+ symbol,
282
+ source: "onchain",
283
+ resolvedAt: Date.now()
284
+ };
285
+ if (symbol) {
286
+ console.log(`TokenResolver: ${symbol} (${addr.slice(0, 10)}...) \u2192 ${decimals} decimals [onchain]`);
287
+ } else {
288
+ console.log(`TokenResolver: ${addr.slice(0, 10)}... \u2192 ${decimals} decimals [onchain]`);
289
+ }
290
+ } else {
291
+ failed.push(addr);
292
+ }
293
+ }
294
+ this.persist();
295
+ } catch (error) {
296
+ console.warn("TokenResolver: Multicall3 failed entirely, trying Blockscout:", error instanceof Error ? error.message : error);
297
+ return addresses;
298
+ }
299
+ return failed;
300
+ }
301
+ /**
302
+ * Tier 3: Fetch token metadata from Blockscout (Base's block explorer API).
303
+ * Free, no API key, near-universal coverage for any token indexed by the explorer.
304
+ * Returns addresses that failed (truly unresolvable).
305
+ */
306
+ async resolveViaBlockscout(addresses) {
307
+ const failed = [];
308
+ const CONCURRENCY = 5;
309
+ for (let i = 0; i < addresses.length; i += CONCURRENCY) {
310
+ const batch = addresses.slice(i, i + CONCURRENCY);
311
+ const results = await Promise.allSettled(
312
+ batch.map(async (addr) => {
313
+ const response = await fetch(
314
+ `https://base.blockscout.com/api/v2/tokens/${addr}`,
315
+ { signal: AbortSignal.timeout(8e3) }
316
+ );
317
+ if (!response.ok) return null;
318
+ const data = await response.json();
319
+ if (data.decimals === null || data.decimals === void 0) return null;
320
+ const decimals = parseInt(data.decimals, 10);
321
+ if (isNaN(decimals)) return null;
322
+ return {
323
+ decimals,
324
+ symbol: data.symbol || data.name || void 0
325
+ };
326
+ })
327
+ );
328
+ for (let j = 0; j < batch.length; j++) {
329
+ const addr = batch[j];
330
+ const result = results[j];
331
+ if (result.status === "fulfilled" && result.value) {
332
+ this.cache[addr] = {
333
+ decimals: result.value.decimals,
334
+ symbol: result.value.symbol,
335
+ source: "blockscout",
336
+ resolvedAt: Date.now()
337
+ };
338
+ const sym = result.value.symbol || addr.slice(0, 10) + "...";
339
+ console.log(`TokenResolver: ${sym} \u2192 ${result.value.decimals} decimals [blockscout]`);
340
+ } else {
341
+ failed.push(addr);
342
+ }
343
+ }
344
+ }
345
+ if (failed.length === 0 || addresses.length > failed.length) {
346
+ this.persist();
347
+ }
348
+ return failed;
349
+ }
350
+ /**
351
+ * Persist resolved metadata to FileStore.
352
+ */
353
+ persist() {
354
+ if (!this.store) return;
355
+ const toSave = {};
356
+ for (const [addr, meta] of Object.entries(this.cache)) {
357
+ if (meta.source !== "hardcoded") {
358
+ toSave[addr] = meta;
359
+ }
360
+ }
361
+ this.store.set(STORE_KEY, toSave);
362
+ }
363
+ /**
364
+ * Persist unresolvable token list to FileStore.
365
+ */
366
+ persistUnresolvable() {
367
+ if (!this.store) return;
368
+ this.store.set(UNRESOLVABLE_KEY, [...this.unresolvable]);
369
+ }
370
+ };
371
+
372
+ // src/trading/market.ts
94
373
  var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
374
+ var _globalResolver = null;
375
+ function setGlobalResolver(resolver) {
376
+ _globalResolver = resolver;
377
+ }
95
378
  var TOKEN_DECIMALS = {
96
379
  [NATIVE_ETH.toLowerCase()]: 18,
97
380
  // Native ETH
@@ -170,54 +453,19 @@ var TOKEN_DECIMALS = {
170
453
  "0x768be13e1680b5ebe0024c42c896e3db59ec0149": 9
171
454
  // SKI
172
455
  };
173
- var decimalsCache = {};
174
- var symbolCache = {};
175
456
  function getTokenDecimals(address) {
176
457
  const key = address.toLowerCase();
458
+ if (_globalResolver) {
459
+ const d = _globalResolver.getDecimals(key);
460
+ if (d !== void 0) return d;
461
+ if (_globalResolver.isUnresolvable(key)) return 18;
462
+ }
177
463
  const known = TOKEN_DECIMALS[key];
178
464
  if (known !== void 0) return known;
179
- const cached = decimalsCache[key];
180
- if (cached !== void 0) return cached;
181
- console.warn(`Unknown token decimals for ${address}, defaulting to 18. Call fetchTokenDecimals() first for accuracy.`);
182
465
  return 18;
183
466
  }
184
467
  function getTokenSymbol(address) {
185
- return symbolCache[address.toLowerCase()];
186
- }
187
- async function fetchTokenDecimals(client, address) {
188
- const key = address.toLowerCase();
189
- const known = TOKEN_DECIMALS[key];
190
- if (known !== void 0) return known;
191
- const cached = decimalsCache[key];
192
- if (cached !== void 0) return cached;
193
- for (let attempt = 0; attempt < 2; attempt++) {
194
- try {
195
- const [decimals, symbol] = await Promise.all([
196
- client.readContract({
197
- address,
198
- abi: import_viem.erc20Abi,
199
- functionName: "decimals"
200
- }),
201
- client.readContract({
202
- address,
203
- abi: import_viem.erc20Abi,
204
- functionName: "symbol"
205
- }).catch(() => void 0)
206
- // symbol is optional — some tokens don't have it
207
- ]);
208
- const result = Number(decimals);
209
- decimalsCache[key] = result;
210
- if (symbol) symbolCache[key] = String(symbol);
211
- return result;
212
- } catch {
213
- if (attempt === 0) {
214
- await new Promise((r) => setTimeout(r, 2e3));
215
- }
216
- }
217
- }
218
- console.warn(`Failed to fetch decimals for ${address} after 2 attempts, defaulting to 18`);
219
- decimalsCache[key] = 18;
220
- return 18;
468
+ return _globalResolver?.getSymbol(address.toLowerCase());
221
469
  }
222
470
  var TOKEN_TO_COINGECKO = {
223
471
  [NATIVE_ETH.toLowerCase()]: "ethereum",
@@ -301,24 +549,31 @@ var PRICE_STALENESS_MS = 6e4;
301
549
  var MarketDataService = class {
302
550
  rpcUrl;
303
551
  client;
552
+ resolver;
304
553
  /** Cached prices from last fetch */
305
554
  cachedPrices = {};
306
555
  /** Timestamp of last successful price fetch */
307
556
  lastPriceFetchAt = 0;
308
- constructor(rpcUrl) {
309
- this.rpcUrl = rpcUrl;
310
- this.client = (0, import_viem.createPublicClient)({
311
- transport: (0, import_viem.http)(rpcUrl, { timeout: 6e4 })
312
- });
313
- }
314
557
  /** Cached volume data */
315
558
  cachedVolume24h = {};
316
559
  /** Cached price change data */
317
560
  cachedPriceChange24h = {};
561
+ constructor(rpcUrl, store) {
562
+ this.rpcUrl = rpcUrl;
563
+ this.client = (0, import_viem2.createPublicClient)({
564
+ transport: (0, import_viem2.http)(rpcUrl, { timeout: 6e4 })
565
+ });
566
+ this.resolver = new TokenResolver(this.client, store);
567
+ }
568
+ /** Get the underlying TokenResolver for direct access */
569
+ getResolver() {
570
+ return this.resolver;
571
+ }
318
572
  /**
319
573
  * Fetch current market data for the agent
320
574
  */
321
575
  async fetchMarketData(walletAddress, tokenAddresses) {
576
+ await this.resolver.resolve(tokenAddresses);
322
577
  const prices = await this.fetchPrices(tokenAddresses);
323
578
  const balances = await this.fetchBalances(walletAddress, tokenAddresses);
324
579
  const portfolioValue = this.calculatePortfolioValue(balances, prices);
@@ -455,14 +710,14 @@ var MarketDataService = class {
455
710
  const liq = pair.liquidity?.usd || 0;
456
711
  if (price > 0 && (!bestPrices[addr] || liq > bestPrices[addr].liquidity)) {
457
712
  bestPrices[addr] = { price, liquidity: liq };
458
- if (pair.baseToken.symbol && !symbolCache[addr]) {
459
- symbolCache[addr] = pair.baseToken.symbol;
713
+ if (pair.baseToken.symbol) {
714
+ this.resolver.setSymbol(addr, pair.baseToken.symbol);
460
715
  }
461
716
  }
462
717
  }
463
718
  for (const [addr, { price }] of Object.entries(bestPrices)) {
464
719
  prices[addr] = price;
465
- const sym = symbolCache[addr] || addr.slice(0, 10);
720
+ const sym = this.resolver.getSymbol(addr) || addr.slice(0, 10);
466
721
  console.log(`DexScreener price for ${sym}: $${price}`);
467
722
  }
468
723
  }
@@ -479,7 +734,7 @@ var MarketDataService = class {
479
734
  }
480
735
  for (const addr of tokenAddresses) {
481
736
  if (!prices[addr.toLowerCase()]) {
482
- const sym = symbolCache[addr.toLowerCase()];
737
+ const sym = this.resolver.getSymbol(addr.toLowerCase());
483
738
  console.warn(`No price available for ${sym ? `${sym} (${addr})` : addr} \u2014 CoinGecko, DeFi Llama, and DexScreener all returned nothing`);
484
739
  prices[addr.toLowerCase()] = 0;
485
740
  }
@@ -487,36 +742,45 @@ var MarketDataService = class {
487
742
  return prices;
488
743
  }
489
744
  /**
490
- * Fetch real on-chain balances: native ETH + ERC-20 tokens
745
+ * Fetch real on-chain balances: native ETH + ERC-20 tokens.
746
+ * Uses Multicall3 to batch all balanceOf calls into a single RPC request.
491
747
  */
492
748
  async fetchBalances(walletAddress, tokenAddresses) {
493
749
  const balances = {};
494
750
  const wallet = walletAddress;
495
751
  try {
496
- const unknownTokens = tokenAddresses.filter(
497
- (addr) => TOKEN_DECIMALS[addr.toLowerCase()] === void 0 && !decimalsCache[addr.toLowerCase()]
498
- );
499
- if (unknownTokens.length > 0) {
500
- await Promise.all(unknownTokens.map((addr) => fetchTokenDecimals(this.client, addr)));
501
- }
502
752
  const nativeBalance = await this.client.getBalance({ address: wallet });
503
753
  balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
504
- const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
505
- try {
506
- const balance = await this.client.readContract({
507
- address: tokenAddress,
508
- abi: import_viem.erc20Abi,
509
- functionName: "balanceOf",
510
- args: [wallet]
511
- });
512
- return { address: tokenAddress.toLowerCase(), balance };
513
- } catch (error) {
514
- return { address: tokenAddress.toLowerCase(), balance: 0n };
754
+ if (tokenAddresses.length > 0) {
755
+ const BALANCE_BATCH_SIZE = 25;
756
+ for (let i = 0; i < tokenAddresses.length; i += BALANCE_BATCH_SIZE) {
757
+ const batch = tokenAddresses.slice(i, i + BALANCE_BATCH_SIZE);
758
+ try {
759
+ const results = await this.client.multicall({
760
+ contracts: batch.map((addr) => ({
761
+ address: addr,
762
+ abi: import_viem2.erc20Abi,
763
+ functionName: "balanceOf",
764
+ args: [wallet]
765
+ })),
766
+ allowFailure: true
767
+ });
768
+ for (let j = 0; j < batch.length; j++) {
769
+ const addr = batch[j].toLowerCase();
770
+ const r = results[j];
771
+ if (r.status === "success") {
772
+ balances[addr] = r.result;
773
+ } else {
774
+ const sym = this.resolver.getSymbol(addr) || addr.slice(0, 10);
775
+ console.warn(` balanceOf reverted for ${sym}`);
776
+ balances[addr] = 0n;
777
+ }
778
+ }
779
+ } catch (e) {
780
+ console.warn(`Multicall3 balanceOf batch failed (${batch.length} tokens): ${e instanceof Error ? e.message : String(e)}`);
781
+ await this.fetchBalancesIndividual(wallet, batch, balances);
782
+ }
515
783
  }
516
- });
517
- const results = await Promise.all(erc20Promises);
518
- for (const { address, balance } of results) {
519
- balances[address] = balance;
520
784
  }
521
785
  } catch (error) {
522
786
  console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
@@ -528,13 +792,44 @@ var MarketDataService = class {
528
792
  return balances;
529
793
  }
530
794
  /**
531
- * Calculate total portfolio value in USD
795
+ * Fallback: fetch ERC-20 balances individually (if multicall fails).
796
+ */
797
+ async fetchBalancesIndividual(wallet, tokenAddresses, balances) {
798
+ const promises = tokenAddresses.map(async (tokenAddress) => {
799
+ try {
800
+ const balance = await this.client.readContract({
801
+ address: tokenAddress,
802
+ abi: import_viem2.erc20Abi,
803
+ functionName: "balanceOf",
804
+ args: [wallet]
805
+ });
806
+ return { address: tokenAddress.toLowerCase(), balance };
807
+ } catch (e) {
808
+ const sym = this.resolver.getSymbol(tokenAddress.toLowerCase()) || tokenAddress.slice(0, 10);
809
+ console.warn(` balanceOf failed for ${sym}: ${e instanceof Error ? e.message : String(e)}`);
810
+ return { address: tokenAddress.toLowerCase(), balance: 0n };
811
+ }
812
+ });
813
+ const results = await Promise.all(promises);
814
+ for (const { address, balance } of results) {
815
+ balances[address] = balance;
816
+ }
817
+ }
818
+ /**
819
+ * Calculate total portfolio value in USD.
820
+ * Skips tokens with unresolvable decimals to avoid corrupted calculations.
532
821
  */
533
822
  calculatePortfolioValue(balances, prices) {
534
823
  let total = 0;
535
824
  for (const [address, balance] of Object.entries(balances)) {
536
825
  const price = prices[address.toLowerCase()] || 0;
537
- const decimals = getTokenDecimals(address);
826
+ if (price === 0) continue;
827
+ const key = address.toLowerCase();
828
+ const resolverDecimals = _globalResolver?.getDecimals(key);
829
+ if (resolverDecimals === void 0 && _globalResolver?.isUnresolvable(key)) {
830
+ continue;
831
+ }
832
+ const decimals = resolverDecimals ?? getTokenDecimals(address);
538
833
  const amount = Number(balance) / Math.pow(10, decimals);
539
834
  total += amount * price;
540
835
  }
@@ -2254,7 +2549,7 @@ var FileStore = class {
2254
2549
  };
2255
2550
 
2256
2551
  // src/vault/manager.ts
2257
- var import_viem2 = require("viem");
2552
+ var import_viem3 = require("viem");
2258
2553
  var import_accounts = require("viem/accounts");
2259
2554
  var import_chains = require("viem/chains");
2260
2555
  var ADDRESSES = {
@@ -2345,12 +2640,12 @@ var VaultManager = class {
2345
2640
  this.account = (0, import_accounts.privateKeyToAccount)(config.walletKey);
2346
2641
  this.chain = import_chains.base;
2347
2642
  const rpcUrl = getRpcUrl();
2348
- const transport = (0, import_viem2.http)(rpcUrl, { timeout: 6e4 });
2349
- this.publicClient = (0, import_viem2.createPublicClient)({
2643
+ const transport = (0, import_viem3.http)(rpcUrl, { timeout: 6e4 });
2644
+ this.publicClient = (0, import_viem3.createPublicClient)({
2350
2645
  chain: this.chain,
2351
2646
  transport
2352
2647
  });
2353
- this.walletClient = (0, import_viem2.createWalletClient)({
2648
+ this.walletClient = (0, import_viem3.createWalletClient)({
2354
2649
  account: this.account,
2355
2650
  chain: this.chain,
2356
2651
  transport
@@ -2998,7 +3293,7 @@ var HyperliquidClient = class {
2998
3293
  };
2999
3294
 
3000
3295
  // src/perp/signer.ts
3001
- var import_viem3 = require("viem");
3296
+ var import_viem4 = require("viem");
3002
3297
  var HYPERLIQUID_DOMAIN = {
3003
3298
  name: "HyperliquidSignTransaction",
3004
3299
  version: "1",
@@ -3072,10 +3367,10 @@ function fillHashToBytes32(fillHash) {
3072
3367
  if (fillHash.startsWith("0x") && fillHash.length === 66) {
3073
3368
  return fillHash;
3074
3369
  }
3075
- return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["string"], [fillHash]));
3370
+ return (0, import_viem4.keccak256)((0, import_viem4.encodePacked)(["string"], [fillHash]));
3076
3371
  }
3077
3372
  function fillOidToBytes32(oid) {
3078
- return (0, import_viem3.keccak256)((0, import_viem3.encodePacked)(["uint256"], [BigInt(oid)]));
3373
+ return (0, import_viem4.keccak256)((0, import_viem4.encodePacked)(["uint256"], [BigInt(oid)]));
3079
3374
  }
3080
3375
 
3081
3376
  // src/perp/orders.ts
@@ -3677,7 +3972,7 @@ var HyperliquidWebSocket = class {
3677
3972
  };
3678
3973
 
3679
3974
  // src/perp/recorder.ts
3680
- var import_viem4 = require("viem");
3975
+ var import_viem5 = require("viem");
3681
3976
  var import_chains2 = require("viem/chains");
3682
3977
  var import_accounts3 = require("viem/accounts");
3683
3978
  var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
@@ -3720,12 +4015,12 @@ var PerpTradeRecorder = class {
3720
4015
  this.configHash = opts.configHash;
3721
4016
  this.account = (0, import_accounts3.privateKeyToAccount)(opts.privateKey);
3722
4017
  const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3723
- const transport = (0, import_viem4.http)(rpcUrl, { timeout: 6e4 });
3724
- this.publicClient = (0, import_viem4.createPublicClient)({
4018
+ const transport = (0, import_viem5.http)(rpcUrl, { timeout: 6e4 });
4019
+ this.publicClient = (0, import_viem5.createPublicClient)({
3725
4020
  chain: import_chains2.base,
3726
4021
  transport
3727
4022
  });
3728
- this.walletClient = (0, import_viem4.createWalletClient)({
4023
+ this.walletClient = (0, import_viem5.createWalletClient)({
3729
4024
  chain: import_chains2.base,
3730
4025
  transport,
3731
4026
  account: this.account
@@ -4033,23 +4328,23 @@ var PerpOnboarding = class {
4033
4328
  };
4034
4329
 
4035
4330
  // src/perp/funding.ts
4036
- var import_viem5 = require("viem");
4331
+ var import_viem6 = require("viem");
4037
4332
  var import_chains3 = require("viem/chains");
4038
4333
  var import_accounts4 = require("viem/accounts");
4039
- var ERC20_ABI = (0, import_viem5.parseAbi)([
4334
+ var ERC20_ABI = (0, import_viem6.parseAbi)([
4040
4335
  "function approve(address spender, uint256 amount) external returns (bool)",
4041
4336
  "function balanceOf(address account) external view returns (uint256)",
4042
4337
  "function allowance(address owner, address spender) external view returns (uint256)"
4043
4338
  ]);
4044
- var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
4339
+ var TOKEN_MESSENGER_V2_ABI = (0, import_viem6.parseAbi)([
4045
4340
  "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
4046
4341
  "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
4047
4342
  ]);
4048
- var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
4343
+ var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem6.parseAbi)([
4049
4344
  "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
4050
4345
  "event MessageSent(bytes message)"
4051
4346
  ]);
4052
- var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
4347
+ var CORE_DEPOSIT_WALLET_ABI = (0, import_viem6.parseAbi)([
4053
4348
  "function deposit(uint256 amount, uint32 destinationDex) external"
4054
4349
  ]);
4055
4350
 
@@ -4139,7 +4434,8 @@ var AgentRuntime = class {
4139
4434
  this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
4140
4435
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
4141
4436
  this.riskManager = new RiskManager(this.config.trading);
4142
- this.marketData = new MarketDataService(this.getRpcUrl());
4437
+ this.marketData = new MarketDataService(this.getRpcUrl(), store);
4438
+ setGlobalResolver(this.marketData.getResolver());
4143
4439
  const savedRisk = this.positionTracker.getRiskState();
4144
4440
  if (savedRisk.lastResetDate) {
4145
4441
  this.riskManager.restoreState(savedRisk);
@@ -4236,9 +4532,9 @@ var AgentRuntime = class {
4236
4532
  this.perpClient = new HyperliquidClient(config);
4237
4533
  const perpKey = perpConfig.perpRelayerKey || this.config.privateKey;
4238
4534
  const account = (0, import_accounts5.privateKeyToAccount)(perpKey);
4239
- const walletClient = (0, import_viem6.createWalletClient)({
4535
+ const walletClient = (0, import_viem7.createWalletClient)({
4240
4536
  chain: { id: 42161, name: "Arbitrum", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } } },
4241
- transport: (0, import_viem6.http)("https://arb1.arbitrum.io/rpc", { timeout: 6e4 }),
4537
+ transport: (0, import_viem7.http)("https://arb1.arbitrum.io/rpc", { timeout: 6e4 }),
4242
4538
  account
4243
4539
  });
4244
4540
  this.perpSigner = new HyperliquidSigner(walletClient);
@@ -4478,9 +4774,9 @@ var AgentRuntime = class {
4478
4774
  const message = error instanceof Error ? error.message : String(error);
4479
4775
  if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
4480
4776
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
4481
- const publicClientInstance = (0, import_viem6.createPublicClient)({
4777
+ const publicClientInstance = (0, import_viem7.createPublicClient)({
4482
4778
  chain: import_chains4.base,
4483
- transport: (0, import_viem6.http)(this.getRpcUrl(), { timeout: 6e4 })
4779
+ transport: (0, import_viem7.http)(this.getRpcUrl(), { timeout: 6e4 })
4484
4780
  });
4485
4781
  console.log("");
4486
4782
  console.log("=== ETH NEEDED FOR GAS ===");
@@ -5232,7 +5528,14 @@ var AgentRuntime = class {
5232
5528
  if (extras.length > 0) {
5233
5529
  console.log(`Auto-tracking ${extras.length} additional token(s) from position history`);
5234
5530
  }
5235
- return [...base5, ...extras];
5531
+ const resolver = this.marketData.getResolver();
5532
+ const allTokens = [...base5, ...extras];
5533
+ const filtered = allTokens.filter((t) => !resolver.isUnresolvable(t.toLowerCase()));
5534
+ const dropped = allTokens.length - filtered.length;
5535
+ if (dropped > 0) {
5536
+ console.log(`Skipping ${dropped} unresolvable token(s)`);
5537
+ }
5538
+ return filtered;
5236
5539
  }
5237
5540
  /**
5238
5541
  * Default tokens to track.
@@ -5563,7 +5866,7 @@ function loadSecureEnv(basePath, passphrase) {
5563
5866
  }
5564
5867
 
5565
5868
  // src/index.ts
5566
- var AGENT_VERSION = "0.1.32";
5869
+ var AGENT_VERSION = "0.1.34";
5567
5870
  // Annotate the CommonJS export names for ESM import in node:
5568
5871
  0 && (module.exports = {
5569
5872
  AGENT_VERSION,