web3-tools-mcp 3.0.0 → 3.1.0

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +76 -52
  3. package/dist/chain-meta.d.ts.map +1 -1
  4. package/dist/chain-meta.js +19 -8
  5. package/dist/chain-meta.js.map +1 -1
  6. package/dist/clear-signing.d.ts.map +1 -1
  7. package/dist/clear-signing.js +17 -6
  8. package/dist/clear-signing.js.map +1 -1
  9. package/dist/http-server.d.ts.map +1 -1
  10. package/dist/http-server.js +9 -3
  11. package/dist/http-server.js.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/kv-storage.d.ts +2 -1
  15. package/dist/kv-storage.d.ts.map +1 -1
  16. package/dist/kv-storage.js +5 -2
  17. package/dist/kv-storage.js.map +1 -1
  18. package/dist/oauth.d.ts +18 -4
  19. package/dist/oauth.d.ts.map +1 -1
  20. package/dist/oauth.js +62 -26
  21. package/dist/oauth.js.map +1 -1
  22. package/dist/preview.d.ts.map +1 -1
  23. package/dist/preview.js +7 -5
  24. package/dist/preview.js.map +1 -1
  25. package/dist/tools/index.d.ts.map +1 -1
  26. package/dist/tools/index.js +14 -1
  27. package/dist/tools/index.js.map +1 -1
  28. package/dist/tools/read/advanced.d.ts +9 -4
  29. package/dist/tools/read/advanced.d.ts.map +1 -1
  30. package/dist/tools/read/advanced.js +45 -33
  31. package/dist/tools/read/advanced.js.map +1 -1
  32. package/dist/tools/read/balance.d.ts.map +1 -1
  33. package/dist/tools/read/balance.js +17 -32
  34. package/dist/tools/read/balance.js.map +1 -1
  35. package/dist/tools/read/contract-info.d.ts +2 -0
  36. package/dist/tools/read/contract-info.d.ts.map +1 -1
  37. package/dist/tools/read/contract-info.js +53 -122
  38. package/dist/tools/read/contract-info.js.map +1 -1
  39. package/dist/tools/read/contract.d.ts.map +1 -1
  40. package/dist/tools/read/contract.js +2 -1
  41. package/dist/tools/read/contract.js.map +1 -1
  42. package/dist/tools/read/ens.d.ts +20 -40
  43. package/dist/tools/read/ens.d.ts.map +1 -1
  44. package/dist/tools/read/ens.js +25 -33
  45. package/dist/tools/read/ens.js.map +1 -1
  46. package/dist/tools/read/gas.d.ts.map +1 -1
  47. package/dist/tools/read/gas.js +13 -48
  48. package/dist/tools/read/gas.js.map +1 -1
  49. package/dist/tools/read/logs.d.ts +61 -0
  50. package/dist/tools/read/logs.d.ts.map +1 -1
  51. package/dist/tools/read/logs.js +96 -115
  52. package/dist/tools/read/logs.js.map +1 -1
  53. package/dist/tools/read/signatures.d.ts.map +1 -1
  54. package/dist/tools/read/signatures.js +4 -3
  55. package/dist/tools/read/signatures.js.map +1 -1
  56. package/dist/tools/read/signing.d.ts.map +1 -1
  57. package/dist/tools/read/signing.js +8 -2
  58. package/dist/tools/read/signing.js.map +1 -1
  59. package/dist/tools/sign/transactions.d.ts +4 -4
  60. package/dist/tools/sign/transactions.d.ts.map +1 -1
  61. package/dist/tools/sign/transactions.js +54 -9
  62. package/dist/tools/sign/transactions.js.map +1 -1
  63. package/dist/utils.d.ts +18 -0
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/utils.js +43 -1
  66. package/dist/utils.js.map +1 -1
  67. package/dist/wallet-client.d.ts +1 -0
  68. package/dist/wallet-client.d.ts.map +1 -1
  69. package/dist/wallet-client.js +16 -3
  70. package/dist/wallet-client.js.map +1 -1
  71. package/package.json +4 -7
  72. package/src/chain-meta.ts +16 -7
  73. package/src/clear-signing.ts +15 -9
  74. package/src/http-server.ts +8 -3
  75. package/src/index.ts +5 -1
  76. package/src/kv-storage.ts +6 -3
  77. package/src/oauth.ts +93 -34
  78. package/src/preview.ts +7 -5
  79. package/src/tools/index.ts +12 -1
  80. package/src/tools/read/advanced.ts +63 -34
  81. package/src/tools/read/balance.ts +18 -33
  82. package/src/tools/read/contract-info.ts +64 -144
  83. package/src/tools/read/contract.ts +2 -1
  84. package/src/tools/read/ens.ts +26 -33
  85. package/src/tools/read/gas.ts +13 -50
  86. package/src/tools/read/logs.ts +131 -143
  87. package/src/tools/read/signatures.ts +4 -4
  88. package/src/tools/read/signing.ts +8 -2
  89. package/src/tools/sign/transactions.ts +60 -9
  90. package/src/utils.ts +47 -1
  91. package/src/wallet-client.ts +17 -3
@@ -2,7 +2,7 @@ import { isAddress } from 'viem'
2
2
  import { z } from 'zod'
3
3
  import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
4
4
  import type { ChainName } from '../../types.js'
5
- import { createTool, formatResponse } from '../../utils.js'
5
+ import { createTool, formatResponse, TtlCache } from '../../utils.js'
6
6
 
7
7
  // Type for raw Etherscan contract info
8
8
  interface EtherscanContractInfo {
@@ -21,66 +21,83 @@ interface EtherscanContractInfo {
21
21
  SwarmSource: string
22
22
  }
23
23
 
24
- // Cache for contract source code and ABIs
25
24
  interface CachedContract {
26
- rawInfo: EtherscanContractInfo // Cache the raw Etherscan response
25
+ rawInfo: EtherscanContractInfo
27
26
  sourceFiles: Record<string, string>
28
- abi?: any[]
29
27
  metadata: {
30
28
  contractName: string
31
29
  compilerVersion: string
32
30
  isProxy: boolean
33
31
  implementationAddress?: string
34
32
  }
35
- timestamp: number
36
33
  }
37
34
 
38
- const contractCache = new Map<string, CachedContract>()
39
- // No TTL - contracts are immutable, cache never expires
35
+ // Source is immutable, but a proxy's implementation is not; an hour notices an upgrade.
36
+ const contractCache = new TtlCache<CachedContract>(100, 60 * 60 * 1000)
40
37
 
41
38
  // Helper to generate cache key
42
39
  function getCacheKey(chainId: number, address: string): string {
43
40
  return `${chainId}:${address.toLowerCase()}`
44
41
  }
45
42
 
46
- // Shared helper to fetch contract info from Etherscan (with caching)
47
- async function fetchContractInfo(chainId: number, address: string, etherscanApiKey: string): Promise<EtherscanContractInfo> {
48
- // Check cache first
49
- const cacheKey = getCacheKey(chainId, address)
50
- const cached = contractCache.get(cacheKey)
43
+ /** Etherscan's `result` for a query, or why there is none: it answers status "0" with the reason there. */
44
+ export async function etherscan<T>(chainId: number, params: string, etherscanApiKey: string): Promise<T> {
45
+ const response = await fetch(`https://api.etherscan.io/v2/api?chainid=${chainId}&${params}&apikey=${etherscanApiKey}`, {
46
+ signal: AbortSignal.timeout(15_000)
47
+ })
48
+ if (!response.ok) throw new Error(`Etherscan API error: HTTP ${response.status}`)
51
49
 
52
- if (cached?.rawInfo) {
53
- return cached.rawInfo
50
+ const data = (await response.json()) as { status: string; message?: string; result: unknown }
51
+ if (data.status !== '1') {
52
+ throw new Error(`Etherscan API error: ${typeof data.result === 'string' ? data.result : data.message || 'request failed'}`)
54
53
  }
54
+ return data.result as T
55
+ }
55
56
 
56
- // Fetch from Etherscan API
57
- const sourceUrl = `https://api.etherscan.io/v2/api?chainid=${chainId}&module=contract&action=getsourcecode&address=${address}&apikey=${etherscanApiKey}`
58
-
59
- const sourceResponse = await fetch(sourceUrl)
60
- const sourceData = (await sourceResponse.json()) as {
61
- status: string
62
- message?: string
63
- result: Array<EtherscanContractInfo>
57
+ /** Etherscan's SourceCode is one file, a `{{…}}`-wrapped standard JSON input, or a plain map of files. */
58
+ function parseSourceFiles(info: EtherscanContractInfo): Record<string, string> {
59
+ const single = { [`${info.ContractName}.sol`]: info.SourceCode }
60
+ if (!info.SourceCode.startsWith('{')) return single
61
+
62
+ try {
63
+ const parsed = JSON.parse(info.SourceCode.startsWith('{{') ? info.SourceCode.slice(1, -1) : info.SourceCode)
64
+ const files = (parsed.sources ?? parsed) as Record<string, string | { content?: string }>
65
+ return Object.fromEntries(
66
+ Object.entries(files).map(([path, data]) => [path, typeof data === 'string' ? data : data.content || ''])
67
+ )
68
+ } catch {
69
+ return single
64
70
  }
71
+ }
65
72
 
66
- if (sourceData.status !== '1' || !sourceData.result || sourceData.result.length === 0) {
67
- throw new Error(`Etherscan API error: ${sourceData.message || 'No contract source found'}`)
68
- }
73
+ function fileStats(sourceFiles: Record<string, string>) {
74
+ return Object.entries(sourceFiles).map(([path, content]) => ({ path, lines: content.split('\n').length, size: content.length }))
75
+ }
69
76
 
70
- const contractInfo = sourceData.result[0]
77
+ // Shared helper to fetch contract info from Etherscan (with caching)
78
+ async function fetchContractInfo(chainId: number, address: string, etherscanApiKey: string): Promise<EtherscanContractInfo> {
79
+ const cacheKey = getCacheKey(chainId, address)
80
+ const cached = contractCache.get(cacheKey)
81
+ if (cached) return cached.rawInfo
82
+
83
+ const result = await etherscan<EtherscanContractInfo[]>(
84
+ chainId,
85
+ `module=contract&action=getsourcecode&address=${address}`,
86
+ etherscanApiKey
87
+ )
88
+ const contractInfo = result[0]
89
+ if (!contractInfo) throw new Error('Etherscan API error: No contract source found')
71
90
 
72
- // Cache the raw info immediately
73
91
  const isProxy = contractInfo.Proxy === '1'
74
92
  contractCache.set(cacheKey, {
75
93
  rawInfo: contractInfo,
76
- sourceFiles: {},
94
+ sourceFiles: contractInfo.SourceCode ? parseSourceFiles(contractInfo) : {},
77
95
  metadata: {
78
96
  contractName: contractInfo.ContractName,
79
97
  compilerVersion: contractInfo.CompilerVersion,
80
98
  isProxy,
81
99
  implementationAddress: isProxy ? contractInfo.Implementation : undefined
82
- },
83
- timestamp: Date.now()
100
+ }
84
101
  })
85
102
 
86
103
  return contractInfo
@@ -102,7 +119,7 @@ export default {
102
119
  }),
103
120
  async (args) => {
104
121
  const clientManager = getClientManager()
105
- const config = (clientManager as any).config
122
+ const config = clientManager.getConfig()
106
123
 
107
124
  if (!config.etherscanApiKey) {
108
125
  throw new Error(
@@ -189,23 +206,16 @@ export default {
189
206
  // Try to get creation info if requested
190
207
  if (includeSet.has('creation')) {
191
208
  try {
192
- const creationUrl = `https://api.etherscan.io/v2/api?chainid=${chainId}&module=contract&action=getcontractcreation&contractaddresses=${args.address}&apikey=${config.etherscanApiKey}`
193
- const creationResponse = await fetch(creationUrl)
194
- const creationData = (await creationResponse.json()) as {
195
- status: string
196
- result?: Array<{ contractCreator: string; txHash: string }>
197
- }
198
- if (creationData.status === '1' && creationData.result && creationData.result.length > 0) {
199
- const result = creationData.result[0]
200
- if (result) {
201
- abiResult.creationInfo = {
202
- creator: result.contractCreator,
203
- transactionHash: result.txHash
204
- }
205
- }
209
+ const [creation] = await etherscan<Array<{ contractCreator: string; txHash: string }>>(
210
+ chainId,
211
+ `module=contract&action=getcontractcreation&contractaddresses=${args.address}`,
212
+ config.etherscanApiKey
213
+ )
214
+ if (creation) {
215
+ abiResult.creationInfo = { creator: creation.contractCreator, transactionHash: creation.txHash }
206
216
  }
207
- } catch {
208
- // Creation info is optional
217
+ } catch (error) {
218
+ abiResult.creationError = (error as Error).message
209
219
  }
210
220
  }
211
221
 
@@ -249,14 +259,6 @@ export default {
249
259
  }
250
260
  }
251
261
 
252
- // Update cache with ABI (rawInfo already cached by fetchContractInfo)
253
- const cacheKey = getCacheKey(chainId, args.address)
254
- const existing = contractCache.get(cacheKey)!
255
- contractCache.set(cacheKey, {
256
- ...existing,
257
- abi
258
- })
259
-
260
262
  return formatResponse(abiResult)
261
263
  }
262
264
  ),
@@ -282,7 +284,7 @@ export default {
282
284
  }),
283
285
  async (args) => {
284
286
  const clientManager = getClientManager()
285
- const config = (clientManager as any).config
287
+ const config = clientManager.getConfig()
286
288
 
287
289
  if (!config.etherscanApiKey) {
288
290
  throw new Error('Etherscan API key is required. Use --etherscan-api-key or set ETHERSCAN_API_KEY environment variable.')
@@ -316,43 +318,8 @@ export default {
316
318
  })
317
319
  }
318
320
 
319
- // Parse source code
320
- let sourceFiles: Record<string, string> = {}
321
- let sourceCodeString = contractInfo.SourceCode
322
-
323
- // Handle multi-file sources (wrapped in {{ }} or [ ])
324
- if (sourceCodeString.startsWith('{{') || sourceCodeString.startsWith('[')) {
325
- try {
326
- // Remove outer braces/brackets if present
327
- if (sourceCodeString.startsWith('{{')) {
328
- sourceCodeString = sourceCodeString.slice(1, -1)
329
- }
330
- const parsed = JSON.parse(sourceCodeString)
331
-
332
- if (parsed.sources) {
333
- // Standard JSON format
334
- sourceFiles = Object.fromEntries(
335
- Object.entries(parsed.sources).map(([path, data]: [string, any]) => [path, data.content || ''])
336
- )
337
- } else if (typeof parsed === 'object') {
338
- // Simple object format
339
- sourceFiles = parsed
340
- }
341
- } catch {
342
- // If parsing fails, treat as single file
343
- sourceFiles = { [`${contractInfo.ContractName}.sol`]: sourceCodeString }
344
- }
345
- } else {
346
- // Single file source
347
- sourceFiles = { [`${contractInfo.ContractName}.sol`]: sourceCodeString }
348
- }
349
-
350
- // Calculate file stats for summary mode
351
- const fileStats = Object.entries(sourceFiles).map(([path, content]) => ({
352
- path,
353
- lines: (content as string).split('\n').length,
354
- size: (content as string).length
355
- }))
321
+ const sourceFiles = parseSourceFiles(contractInfo)
322
+ const files = fileStats(sourceFiles)
356
323
 
357
324
  const result: Record<string, unknown> = {
358
325
  success: true,
@@ -369,7 +336,7 @@ export default {
369
336
  licenseType: contractInfo.LicenseType,
370
337
  constructorArguments: contractInfo.ConstructorArguments,
371
338
  fileCount: Object.keys(sourceFiles).length,
372
- totalLines: fileStats.reduce((sum, stat) => sum + stat.lines, 0),
339
+ totalLines: files.reduce((sum, stat) => sum + stat.lines, 0),
373
340
  etherscanUrl: clientManager.explorerUrl(args.chain as ChainName, `/address/${args.address}#code`)
374
341
  }
375
342
 
@@ -377,7 +344,7 @@ export default {
377
344
  if (args.includeSource === 'full') {
378
345
  result.sourceFiles = sourceFiles
379
346
  } else if (args.includeSource === 'summary') {
380
- result.files = fileStats
347
+ result.files = files
381
348
  }
382
349
  // 'none' mode: no source files added
383
350
 
@@ -387,37 +354,8 @@ export default {
387
354
  const implInfo = await fetchContractInfo(chainId, contractInfo.Implementation, config.etherscanApiKey)
388
355
 
389
356
  if (implInfo && implInfo.SourceCode !== '') {
390
- // Parse implementation source code
391
- let implSourceFiles: Record<string, string> = {}
392
- let implSourceCodeString = implInfo.SourceCode
393
-
394
- if (implSourceCodeString.startsWith('{{') || implSourceCodeString.startsWith('[')) {
395
- try {
396
- if (implSourceCodeString.startsWith('{{')) {
397
- implSourceCodeString = implSourceCodeString.slice(1, -1)
398
- }
399
- const parsed = JSON.parse(implSourceCodeString)
400
-
401
- if (parsed.sources) {
402
- implSourceFiles = Object.fromEntries(
403
- Object.entries(parsed.sources).map(([path, data]: [string, any]) => [path, data.content || ''])
404
- )
405
- } else if (typeof parsed === 'object') {
406
- implSourceFiles = parsed
407
- }
408
- } catch {
409
- implSourceFiles = { [`${implInfo.ContractName}.sol`]: implSourceCodeString }
410
- }
411
- } else {
412
- implSourceFiles = { [`${implInfo.ContractName}.sol`]: implSourceCodeString }
413
- }
414
-
415
- // Calculate implementation file stats
416
- const implFileStats = Object.entries(implSourceFiles).map(([path, content]) => ({
417
- path,
418
- lines: (content as string).split('\n').length,
419
- size: (content as string).length
420
- }))
357
+ const implSourceFiles = parseSourceFiles(implInfo)
358
+ const implFileStats = fileStats(implSourceFiles)
421
359
 
422
360
  const implResult: Record<string, unknown> = {
423
361
  address: contractInfo.Implementation,
@@ -440,16 +378,6 @@ export default {
440
378
  }
441
379
 
442
380
  result.implementation = implResult
443
-
444
- // Update cache with implementation source (rawInfo already cached by fetchContractInfo)
445
- if (contractInfo.Implementation) {
446
- const implCacheKey = getCacheKey(chainId, contractInfo.Implementation)
447
- const existingImpl = contractCache.get(implCacheKey)!
448
- contractCache.set(implCacheKey, {
449
- ...existingImpl,
450
- sourceFiles: implSourceFiles
451
- })
452
- }
453
381
  }
454
382
  } catch (error) {
455
383
  // Implementation fetch failed, continue without it
@@ -459,14 +387,6 @@ export default {
459
387
  result.implementationAddress = contractInfo.Implementation
460
388
  }
461
389
 
462
- // Update cache with main contract source (rawInfo already cached by fetchContractInfo)
463
- const cacheKey = getCacheKey(chainId, args.address)
464
- const existing = contractCache.get(cacheKey)!
465
- contractCache.set(cacheKey, {
466
- ...existing,
467
- sourceFiles
468
- })
469
-
470
390
  return formatResponse(result)
471
391
  }
472
392
  ),
@@ -2,7 +2,7 @@ import { type AbiFunction, type Address, isAddress, parseAbiItem } from 'viem'
2
2
  import { z } from 'zod'
3
3
  import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
4
4
  import type { ChainName } from '../../types.js'
5
- import { convertArgumentsToTypes, createTool, formatResponse } from '../../utils.js'
5
+ import { convertArgumentsToTypes, createTool, formatResponse, MAX_BATCH } from '../../utils.js'
6
6
 
7
7
  const ContractCallSchema = z.object({
8
8
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network (mainnet, base, arbitrum, polygon, optimism, celo, localhost)'),
@@ -27,6 +27,7 @@ export default {
27
27
  z.object({
28
28
  calls: z
29
29
  .array(ContractCallSchema)
30
+ .max(MAX_BATCH)
30
31
  .describe('Array of contract function calls. Automatically batched by chain/block for optimal performance.')
31
32
  }),
32
33
  async (args) => {
@@ -1,9 +1,9 @@
1
1
  import { type Address, isAddress, namehash } from 'viem'
2
2
  import { normalize } from 'viem/ens'
3
3
  import { z } from 'zod'
4
- import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
4
+ import { getClientManager } from '../../client.js'
5
5
  import type { ChainName } from '../../types.js'
6
- import { createTool, formatResponse } from '../../utils.js'
6
+ import { createTool, formatResponse, MAX_BATCH } from '../../utils.js'
7
7
 
8
8
  // Base ENS Contract Addresses
9
9
  const BASE_ENS_CONTRACTS = {
@@ -159,32 +159,32 @@ async function getTextRecordBase(name: string, key: string): Promise<string | nu
159
159
  }
160
160
  }
161
161
 
162
+ /** Where a name service is deployed: ENS on mainnet, Linea Names on Linea, Basenames on Base. */
163
+ const ENS_CHAINS = ['mainnet', 'linea', 'base'] as const
164
+ const ensChain = z
165
+ .enum(ENS_CHAINS)
166
+ .default('mainnet')
167
+ .describe('Where the name lives: mainnet for .eth (ENS), linea for Linea Names, base for Basenames. Defaults to mainnet.')
168
+
162
169
  const EnsResolveSchema = z.object({
163
- chain: z
164
- .enum(SUPPORTED_CHAINS)
165
- .default('mainnet')
166
- .describe('Blockchain network (defaults to mainnet, as ENS is primarily on Ethereum)'),
167
- name: z.string().describe("ENS name to resolve to an address (e.g., 'vitalik.eth', 'example.eth')"),
168
- universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
170
+ chain: ensChain,
171
+ name: z.string().describe("ENS name to resolve to an address (e.g., 'vitalik.eth', 'example.eth')")
169
172
  })
170
173
 
171
174
  const EnsReverseResolveSchema = z.object({
172
- chain: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
173
- address: z.string().describe('Ethereum address to resolve to an ENS name'),
174
- universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
175
+ chain: ensChain,
176
+ address: z.string().describe('Ethereum address to resolve to an ENS name')
175
177
  })
176
178
 
177
179
  const EnsTextRecordSchema = z.object({
178
- chain: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
180
+ chain: ensChain,
179
181
  name: z.string().describe('ENS name to query text records from'),
180
- key: z.string().describe("Text record key (e.g., 'avatar', 'description', 'email', 'url', 'com.twitter', 'com.github')"),
181
- universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
182
+ key: z.string().describe("Text record key (e.g., 'avatar', 'description', 'email', 'url', 'com.twitter', 'com.github')")
182
183
  })
183
184
 
184
185
  const EnsBatchResolveSchema = z.object({
185
- chain: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
186
- names: z.array(z.string()).describe('Array of ENS names to resolve to addresses in a single batch'),
187
- universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
186
+ chain: ensChain,
187
+ names: z.array(z.string()).max(MAX_BATCH).describe('Array of ENS names to resolve to addresses in a single batch')
188
188
  })
189
189
 
190
190
  export default {
@@ -207,8 +207,7 @@ export default {
207
207
  const client = clientManager.getClient(args.chain as ChainName)
208
208
 
209
209
  address = await client.getEnsAddress({
210
- name: normalizedName,
211
- universalResolverAddress: args.universalResolver !== false ? undefined : undefined
210
+ name: normalizedName
212
211
  })
213
212
  }
214
213
 
@@ -253,8 +252,7 @@ export default {
253
252
  const client = clientManager.getClient(args.chain as ChainName)
254
253
 
255
254
  ensName = await client.getEnsName({
256
- address: args.address as Address,
257
- universalResolverAddress: args.universalResolver !== false ? undefined : undefined
255
+ address: args.address as Address
258
256
  })
259
257
  }
260
258
 
@@ -297,8 +295,7 @@ export default {
297
295
 
298
296
  textRecord = await client.getEnsText({
299
297
  name: normalizedName,
300
- key: args.key,
301
- universalResolverAddress: args.universalResolver !== false ? undefined : undefined
298
+ key: args.key
302
299
  })
303
300
  }
304
301
 
@@ -327,11 +324,10 @@ export default {
327
324
 
328
325
  get_ens_avatar: createTool(
329
326
  'Get ENS Avatar',
330
- 'Get the avatar URI for an ENS name. Returns the avatar URL if set.',
327
+ 'Get the raw avatar text record for an ENS name: an https/ipfs URL, a data URI, or an NFT reference (eip155:1/erc721:0x…/1). It is not fetched or resolved to an image.',
331
328
  z.object({
332
- chain: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
333
- name: z.string().describe('ENS name to get avatar from'),
334
- universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
329
+ chain: ensChain,
330
+ name: z.string().describe('ENS name to get avatar from')
335
331
  }),
336
332
  async (args) => {
337
333
  // Normalize the ENS name
@@ -347,10 +343,8 @@ export default {
347
343
  const clientManager = getClientManager()
348
344
  const client = clientManager.getClient(args.chain as ChainName)
349
345
 
350
- avatar = await client.getEnsAvatar({
351
- name: normalizedName,
352
- universalResolverAddress: args.universalResolver !== false ? undefined : undefined
353
- })
346
+ // getEnsAvatar would fetch the record's URL (or the NFT's metadata) from this server.
347
+ avatar = await client.getEnsText({ name: normalizedName, key: 'avatar' })
354
348
  }
355
349
 
356
350
  if (!avatar) {
@@ -394,8 +388,7 @@ export default {
394
388
  const client = clientManager.getClient(args.chain as ChainName)
395
389
 
396
390
  address = await client.getEnsAddress({
397
- name: normalizedName,
398
- universalResolverAddress: args.universalResolver !== false ? undefined : undefined
391
+ name: normalizedName
399
392
  })
400
393
  }
401
394
 
@@ -4,6 +4,7 @@ import {
4
4
  decodeFunctionResult,
5
5
  encodeFunctionData,
6
6
  formatEther,
7
+ formatGwei,
7
8
  type Hex,
8
9
  isAddress,
9
10
  parseAbiItem,
@@ -14,12 +15,10 @@ import { z } from 'zod'
14
15
  import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
15
16
  import { enrichTransfers } from '../../preview.js'
16
17
  import type { ChainName } from '../../types.js'
17
- import { convertArgumentsToTypes, createTool, formatResponse } from '../../utils.js'
18
+ import { convertArgumentsToTypes, createTool, formatResponse, MAX_BATCH, rpcReason } from '../../utils.js'
18
19
 
19
20
  /** Alchemy's bundle method takes at most this many transactions. */
20
21
  const ALCHEMY_BUNDLE_LIMIT = 3
21
- /** One tool call is one quota unit, so an unbounded bundle would be a way around the quota. */
22
- const MAX_BUNDLE = 25
23
22
  /** Where eth_simulateV1's traced native transfers say they come from. */
24
23
  const NATIVE_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
25
24
 
@@ -67,15 +66,6 @@ interface AlchemyOutcome {
67
66
  error?: { message: string }
68
67
  }
69
68
 
70
- /**
71
- * Why an RPC call failed, without the request URL — viem puts it in `message`, and with a
72
- * provider key configured that URL carries the key.
73
- */
74
- function rpcReason(error: unknown): string {
75
- const { details, shortMessage } = error as { details?: string; shortMessage?: string }
76
- return details || shortMessage || 'The RPC request failed'
77
- }
78
-
79
69
  function bundleCall(
80
70
  call: { to: string; functionAbi?: string; args?: z.infer<typeof argsSchema>; data?: string; value?: string; from?: string },
81
71
  from: string | undefined,
@@ -223,33 +213,16 @@ export default {
223
213
  const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
224
214
  const convertedArgs = convertArgumentsToTypes(args.args || [], abiItem.inputs)
225
215
 
226
- const blockTag = args.blockNumber ? BigInt(args.blockNumber) : undefined
227
-
228
- // Simulate the call
229
- const result = await client.call({
216
+ const tx = {
230
217
  to: args.contractAddress as Address,
231
- data: encodeFunctionData({
232
- abi: [abiItem],
233
- functionName: abiItem.name,
234
- args: convertedArgs
235
- }),
218
+ data: encodeFunctionData({ abi: [abiItem], functionName: abiItem.name, args: convertedArgs }),
236
219
  account: args.from ? (args.from as Address) : undefined,
237
220
  value: args.value ? BigInt(args.value) : undefined,
238
- blockNumber: blockTag
239
- })
221
+ blockNumber: args.blockNumber ? BigInt(args.blockNumber) : undefined
222
+ }
240
223
 
241
- // Also estimate gas
242
- const gasEstimate = await client.estimateGas({
243
- to: args.contractAddress as Address,
244
- data: encodeFunctionData({
245
- abi: [abiItem],
246
- functionName: abiItem.name,
247
- args: convertedArgs
248
- }),
249
- account: args.from ? (args.from as Address) : undefined,
250
- value: args.value ? BigInt(args.value) : undefined,
251
- blockNumber: blockTag
252
- })
224
+ const result = await client.call(tx)
225
+ const gasEstimate = await client.estimateGas(tx)
253
226
 
254
227
  // Decode the result if the function has outputs
255
228
  let decodedResult: unknown = result.data
@@ -272,15 +245,12 @@ export default {
272
245
  blockNumber: args.blockNumber || 'latest'
273
246
  })
274
247
  } catch (error) {
275
- // Check if it's a revert error
276
- const errorMessage = error instanceof Error ? error.message : String(error)
277
-
278
248
  return formatResponse({
279
249
  success: false,
280
250
  chain: args.chain,
281
251
  contractAddress: args.contractAddress,
282
- error: errorMessage,
283
- reverted: errorMessage.includes('revert') || errorMessage.includes('execution reverted')
252
+ error: rpcReason(error),
253
+ reverted: String(error instanceof Error ? error.message : error).includes('revert')
284
254
  })
285
255
  }
286
256
  }
@@ -311,7 +281,7 @@ export default {
311
281
  })
312
282
  )
313
283
  .min(1)
314
- .max(MAX_BUNDLE)
284
+ .max(MAX_BATCH)
315
285
  .describe('Transactions to run, in order'),
316
286
  blockNumber: z.string().optional().describe('Block to simulate on top of (defaults to latest)')
317
287
  }),
@@ -437,14 +407,7 @@ export default {
437
407
  client.estimateFeesPerGas().catch(() => null) // Some chains don't support EIP-1559
438
408
  ])
439
409
 
440
- const formatPrice = (wei: bigint): string => {
441
- if (args.formatted) {
442
- // Convert to Gwei (1 Gwei = 1e9 wei)
443
- const gwei = Number(wei) / 1e9
444
- return `${gwei.toFixed(2)} Gwei`
445
- }
446
- return wei.toString()
447
- }
410
+ const formatPrice = (wei: bigint): string => `${formatGwei(wei)} Gwei`
448
411
 
449
412
  const response: any = {
450
413
  chain: args.chain,
@@ -470,7 +433,7 @@ export default {
470
433
  const standardGasLimit = 21000n
471
434
  const estimatedCost = feeData.maxFeePerGas * standardGasLimit
472
435
  response.eip1559.estimatedCostFor21kGas = args.formatted
473
- ? `${(Number(estimatedCost) / 1e18).toFixed(6)} ETH`
436
+ ? `${formatEther(estimatedCost)} ${client.chain?.nativeCurrency.symbol ?? 'ETH'}`
474
437
  : estimatedCost.toString()
475
438
  }
476
439