web3-tools-mcp 1.0.1 → 1.2.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 (93) hide show
  1. package/README.md +46 -230
  2. package/dist/package.json +63 -0
  3. package/dist/src/client.d.ts +1653 -0
  4. package/dist/src/client.d.ts.map +1 -0
  5. package/dist/src/client.js +142 -0
  6. package/dist/src/client.js.map +1 -0
  7. package/dist/src/index.d.ts +3 -0
  8. package/dist/src/index.d.ts.map +1 -0
  9. package/dist/src/index.js +88 -0
  10. package/dist/src/index.js.map +1 -0
  11. package/dist/src/tools/advanced.d.ts +75 -0
  12. package/dist/src/tools/advanced.d.ts.map +1 -0
  13. package/dist/src/tools/advanced.js +234 -0
  14. package/dist/src/tools/advanced.js.map +1 -0
  15. package/dist/src/tools/balance.d.ts +55 -0
  16. package/dist/src/tools/balance.d.ts.map +1 -0
  17. package/dist/src/tools/balance.js +146 -0
  18. package/dist/src/tools/balance.js.map +1 -0
  19. package/dist/src/tools/contract-info.d.ts +79 -0
  20. package/dist/src/tools/contract-info.d.ts.map +1 -0
  21. package/dist/src/tools/contract-info.js +450 -0
  22. package/dist/src/tools/contract-info.js.map +1 -0
  23. package/dist/src/tools/contract.d.ts +79 -0
  24. package/dist/src/tools/contract.d.ts.map +1 -0
  25. package/dist/src/tools/contract.js +140 -0
  26. package/dist/src/tools/contract.js.map +1 -0
  27. package/dist/src/tools/ens.d.ts +119 -0
  28. package/dist/src/tools/ens.d.ts.map +1 -0
  29. package/dist/src/tools/ens.js +425 -0
  30. package/dist/src/tools/ens.js.map +1 -0
  31. package/dist/src/tools/index.d.ts +3 -0
  32. package/dist/src/tools/index.d.ts.map +1 -0
  33. package/dist/src/tools/index.js +29 -0
  34. package/dist/src/tools/index.js.map +1 -0
  35. package/dist/src/tools/logs.d.ts +39 -0
  36. package/dist/src/tools/logs.d.ts.map +1 -0
  37. package/dist/src/tools/logs.js +231 -0
  38. package/dist/src/tools/logs.js.map +1 -0
  39. package/dist/src/tools/signatures.d.ts +83 -0
  40. package/dist/src/tools/signatures.d.ts.map +1 -0
  41. package/dist/src/tools/signatures.js +93 -0
  42. package/dist/src/tools/signatures.js.map +1 -0
  43. package/dist/src/types.d.ts +22 -0
  44. package/dist/src/types.d.ts.map +1 -0
  45. package/dist/src/types.js +2 -0
  46. package/dist/src/types.js.map +1 -0
  47. package/dist/src/utils.d.ts +22 -0
  48. package/dist/src/utils.d.ts.map +1 -0
  49. package/dist/src/utils.js +183 -0
  50. package/dist/src/utils.js.map +1 -0
  51. package/dist/test/tools/advanced.test.d.ts +2 -0
  52. package/dist/test/tools/advanced.test.d.ts.map +1 -0
  53. package/dist/test/tools/advanced.test.js +235 -0
  54. package/dist/test/tools/advanced.test.js.map +1 -0
  55. package/dist/test/tools/balance.test.d.ts +2 -0
  56. package/dist/test/tools/balance.test.d.ts.map +1 -0
  57. package/dist/test/tools/balance.test.js +167 -0
  58. package/dist/test/tools/balance.test.js.map +1 -0
  59. package/dist/test/tools/contract-info.test.d.ts +2 -0
  60. package/dist/test/tools/contract-info.test.d.ts.map +1 -0
  61. package/dist/test/tools/contract-info.test.js +286 -0
  62. package/dist/test/tools/contract-info.test.js.map +1 -0
  63. package/dist/test/tools/contract.test.d.ts +2 -0
  64. package/dist/test/tools/contract.test.d.ts.map +1 -0
  65. package/dist/test/tools/contract.test.js +234 -0
  66. package/dist/test/tools/contract.test.js.map +1 -0
  67. package/dist/test/tools/ens.test.d.ts +2 -0
  68. package/dist/test/tools/ens.test.d.ts.map +1 -0
  69. package/dist/test/tools/ens.test.js +236 -0
  70. package/dist/test/tools/ens.test.js.map +1 -0
  71. package/dist/test/tools/logs.test.d.ts +2 -0
  72. package/dist/test/tools/logs.test.d.ts.map +1 -0
  73. package/dist/test/tools/logs.test.js +196 -0
  74. package/dist/test/tools/logs.test.js.map +1 -0
  75. package/dist/test/tools/signatures.test.d.ts +2 -0
  76. package/dist/test/tools/signatures.test.d.ts.map +1 -0
  77. package/dist/test/tools/signatures.test.js +112 -0
  78. package/dist/test/tools/signatures.test.js.map +1 -0
  79. package/package.json +37 -22
  80. package/src/client.ts +160 -0
  81. package/src/index.ts +96 -0
  82. package/src/tools/advanced.ts +262 -0
  83. package/src/tools/balance.ts +176 -0
  84. package/src/tools/contract-info.ts +582 -0
  85. package/src/tools/contract.ts +180 -0
  86. package/src/tools/ens.ts +493 -0
  87. package/src/tools/index.ts +37 -0
  88. package/src/tools/logs.ts +291 -0
  89. package/src/tools/signatures.ts +126 -0
  90. package/src/types.ts +29 -0
  91. package/src/utils.ts +222 -0
  92. package/dist/hypersync-client.darwin-arm64-7fegfwn5.node +0 -0
  93. package/dist/index.js +0 -28282
@@ -0,0 +1,582 @@
1
+ import { isAddress } from 'viem'
2
+ import { z } from 'zod'
3
+ import type { ChainName } from '../types.js'
4
+ import { CHAINS, getClientManager, SUPPORTED_CHAINS } from '../client.js'
5
+ import { createTool, formatResponse } from '../utils.js'
6
+
7
+ // Type for raw Etherscan contract info
8
+ interface EtherscanContractInfo {
9
+ SourceCode: string
10
+ ABI: string
11
+ ContractName: string
12
+ CompilerVersion: string
13
+ OptimizationUsed: string
14
+ Runs: string
15
+ ConstructorArguments: string
16
+ EVMVersion: string
17
+ Library: string
18
+ LicenseType: string
19
+ Proxy: string
20
+ Implementation: string
21
+ SwarmSource: string
22
+ }
23
+
24
+ // Cache for contract source code and ABIs
25
+ interface CachedContract {
26
+ rawInfo: EtherscanContractInfo // Cache the raw Etherscan response
27
+ sourceFiles: Record<string, string>
28
+ abi?: any[]
29
+ metadata: {
30
+ contractName: string
31
+ compilerVersion: string
32
+ isProxy: boolean
33
+ implementationAddress?: string
34
+ }
35
+ timestamp: number
36
+ }
37
+
38
+ const contractCache = new Map<string, CachedContract>()
39
+ // No TTL - contracts are immutable, cache never expires
40
+
41
+ // Helper to generate cache key
42
+ function getCacheKey(chainId: number, address: string): string {
43
+ return `${chainId}:${address.toLowerCase()}`
44
+ }
45
+
46
+ // Shared helper to fetch contract info from Etherscan (with caching)
47
+ async function fetchContractInfo(
48
+ chainId: number,
49
+ address: string,
50
+ etherscanApiKey: string
51
+ ): Promise<EtherscanContractInfo> {
52
+ // Check cache first
53
+ const cacheKey = getCacheKey(chainId, address)
54
+ const cached = contractCache.get(cacheKey)
55
+
56
+ if (cached?.rawInfo) {
57
+ return cached.rawInfo
58
+ }
59
+
60
+ // Fetch from Etherscan API
61
+ const sourceUrl = `https://api.etherscan.io/v2/api?chainid=${chainId}&module=contract&action=getsourcecode&address=${address}&apikey=${etherscanApiKey}`
62
+
63
+ const sourceResponse = await fetch(sourceUrl)
64
+ const sourceData = (await sourceResponse.json()) as {
65
+ status: string
66
+ message?: string
67
+ result: Array<EtherscanContractInfo>
68
+ }
69
+
70
+ if (sourceData.status !== '1' || !sourceData.result || sourceData.result.length === 0) {
71
+ throw new Error(`Etherscan API error: ${sourceData.message || 'No contract source found'}`)
72
+ }
73
+
74
+ const contractInfo = sourceData.result[0]
75
+
76
+ // Cache the raw info immediately
77
+ const isProxy = contractInfo.Proxy === '1'
78
+ contractCache.set(cacheKey, {
79
+ rawInfo: contractInfo,
80
+ sourceFiles: {},
81
+ metadata: {
82
+ contractName: contractInfo.ContractName,
83
+ compilerVersion: contractInfo.CompilerVersion,
84
+ isProxy,
85
+ implementationAddress: isProxy ? contractInfo.Implementation : undefined
86
+ },
87
+ timestamp: Date.now()
88
+ })
89
+
90
+ return contractInfo
91
+ }
92
+
93
+ export default {
94
+ get_contract_abi: createTool(
95
+ 'Get Contract ABI',
96
+ 'Retrieve contract ABI, proxy info, and verification status from Etherscan. Use for contract analysis.',
97
+ z.object({
98
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
99
+ address: z.string().describe('The contract address to get ABI for'),
100
+ include: z
101
+ .array(
102
+ z.enum(['abi', 'implementationAbi', 'metadata', 'compilation', 'creation', 'stats'])
103
+ )
104
+ .optional()
105
+ .describe(
106
+ 'Optional: specify which data to include. Options: "abi" (contract ABI), "implementationAbi" (proxy implementation ABI), "metadata" (name, verification), "compilation" (compiler, optimization), "creation" (creator, tx), "stats" (function/event counts). Defaults to ["abi", "metadata"] for minimal context usage.'
107
+ )
108
+ }),
109
+ async (args) => {
110
+ const clientManager = getClientManager()
111
+ const config = (clientManager as any).config
112
+
113
+ if (!config.etherscanApiKey) {
114
+ throw new Error(
115
+ 'Etherscan API key is required for contract information retrieval. Use --etherscan-api-key or set ETHERSCAN_API_KEY environment variable.'
116
+ )
117
+ }
118
+
119
+ if (!isAddress(args.address)) {
120
+ throw new Error('Invalid contract address')
121
+ }
122
+
123
+ // Default to minimal data if not specified
124
+ const include = args.include || ['abi', 'metadata']
125
+ const includeSet = new Set(include)
126
+
127
+ const chains = CHAINS(config)
128
+ const chainId = chains[args.chain as ChainName].chain.id
129
+
130
+ // Use shared helper to fetch contract info
131
+ const contractInfo = await fetchContractInfo(chainId, args.address, config.etherscanApiKey)
132
+
133
+ // Parse ABI if requested
134
+ let abi: Array<{ type: string; [key: string]: unknown }> = []
135
+ if (includeSet.has('abi') || includeSet.has('stats')) {
136
+ if (contractInfo.ABI && contractInfo.ABI !== 'Contract source code not verified') {
137
+ try {
138
+ abi = JSON.parse(contractInfo.ABI)
139
+ } catch {
140
+ // ABI parsing failed, continue without it
141
+ }
142
+ }
143
+ }
144
+
145
+ // Parse the source information
146
+ const isProxy = contractInfo.Proxy === '1'
147
+ const hasSourceCode = contractInfo.SourceCode !== ''
148
+ const isVerified = hasSourceCode
149
+
150
+ // Build base result
151
+ const abiResult: Record<string, unknown> = {
152
+ success: true,
153
+ chain: args.chain,
154
+ chainId,
155
+ address: args.address,
156
+ etherscanUrl: `https://${clientManager.getEtherscanDomain(args.chain as ChainName)}/address/${args.address}`
157
+ }
158
+
159
+ // Add metadata if requested
160
+ if (includeSet.has('metadata')) {
161
+ abiResult.isVerified = isVerified
162
+ abiResult.contractName = contractInfo.ContractName
163
+ abiResult.hasSourceCode = hasSourceCode
164
+ abiResult.licenseType = contractInfo.LicenseType
165
+ if (isProxy) {
166
+ abiResult.isProxy = isProxy
167
+ if (contractInfo.Implementation) {
168
+ abiResult.implementationAddress = contractInfo.Implementation
169
+ }
170
+ }
171
+ }
172
+
173
+ // Add compilation info if requested
174
+ if (includeSet.has('compilation')) {
175
+ abiResult.compilerVersion = contractInfo.CompilerVersion
176
+ abiResult.optimizationUsed = contractInfo.OptimizationUsed === '1'
177
+ abiResult.optimizationRuns = contractInfo.Runs
178
+ abiResult.evmVersion = contractInfo.EVMVersion
179
+ abiResult.constructorArguments = contractInfo.ConstructorArguments
180
+ }
181
+
182
+ // Add ABI if requested
183
+ if (includeSet.has('abi')) {
184
+ abiResult.abi = abi
185
+ }
186
+
187
+ // Add stats if requested
188
+ if (includeSet.has('stats')) {
189
+ abiResult.abiSize = abi.length
190
+ abiResult.functions = abi.filter((item: { type: string }) => item.type === 'function').length
191
+ abiResult.events = abi.filter((item: { type: string }) => item.type === 'event').length
192
+ abiResult.errors = abi.filter((item: { type: string }) => item.type === 'error').length
193
+ abiResult.constructors = abi.filter((item: { type: string }) => item.type === 'constructor').length
194
+ }
195
+
196
+ // Try to get creation info if requested
197
+ if (includeSet.has('creation')) {
198
+ try {
199
+ const creationUrl = `https://api.etherscan.io/v2/api?chainid=${chainId}&module=contract&action=getcontractcreation&contractaddresses=${args.address}&apikey=${config.etherscanApiKey}`
200
+ const creationResponse = await fetch(creationUrl)
201
+ const creationData = (await creationResponse.json()) as {
202
+ status: string
203
+ result?: Array<{ contractCreator: string; txHash: string }>
204
+ }
205
+ if (creationData.status === '1' && creationData.result && creationData.result.length > 0) {
206
+ const result = creationData.result[0]
207
+ if (result) {
208
+ abiResult.creationInfo = {
209
+ creator: result.contractCreator,
210
+ transactionHash: result.txHash
211
+ }
212
+ }
213
+ }
214
+ } catch {
215
+ // Creation info is optional
216
+ }
217
+ }
218
+
219
+ // If it's a proxy and implementation ABI is requested, try to get it
220
+ if (
221
+ includeSet.has('implementationAbi') &&
222
+ isProxy &&
223
+ contractInfo.Implementation &&
224
+ isAddress(contractInfo.Implementation)
225
+ ) {
226
+ try {
227
+ const implContractInfo = await fetchContractInfo(chainId, contractInfo.Implementation, config.etherscanApiKey)
228
+
229
+ if (implContractInfo?.ABI && implContractInfo.ABI !== 'Contract source code not verified') {
230
+ try {
231
+ const implementationAbi = JSON.parse(implContractInfo.ABI)
232
+ abiResult.implementationAbi = implementationAbi
233
+
234
+ // Add implementation stats if stats are requested
235
+ if (includeSet.has('stats')) {
236
+ abiResult.implementationAbiSize = implementationAbi.length
237
+ abiResult.implementationFunctions = implementationAbi.filter(
238
+ (item: { type: string }) => item.type === 'function'
239
+ ).length
240
+ abiResult.implementationEvents = implementationAbi.filter(
241
+ (item: { type: string }) => item.type === 'event'
242
+ ).length
243
+ abiResult.implementationErrors = implementationAbi.filter(
244
+ (item: { type: string }) => item.type === 'error'
245
+ ).length
246
+ abiResult.implementationConstructors = implementationAbi.filter(
247
+ (item: { type: string }) => item.type === 'constructor'
248
+ ).length
249
+ }
250
+ } catch {
251
+ // Implementation ABI parsing failed, continue without it
252
+ }
253
+ }
254
+ } catch {
255
+ // Implementation ABI fetch failed, continue without it
256
+ }
257
+ }
258
+
259
+ // Update cache with ABI (rawInfo already cached by fetchContractInfo)
260
+ const cacheKey = getCacheKey(chainId, args.address)
261
+ const existing = contractCache.get(cacheKey)!
262
+ contractCache.set(cacheKey, {
263
+ ...existing,
264
+ abi
265
+ })
266
+
267
+ return formatResponse(abiResult)
268
+ }
269
+ ),
270
+
271
+ get_contract_source_code: createTool(
272
+ 'Get Contract Source Code',
273
+ 'Retrieve verified contract source code from Etherscan. PROXY AWARE - automatically fetches implementation source for proxies.',
274
+ z.object({
275
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
276
+ address: z.string().describe('The contract address to get source code for'),
277
+ includeImplementation: z
278
+ .boolean()
279
+ .optional()
280
+ .default(true)
281
+ .describe('For proxy contracts, also fetch implementation source code (default: true)'),
282
+ includeSource: z
283
+ .enum(['full', 'summary', 'none'])
284
+ .optional()
285
+ .default('summary')
286
+ .describe(
287
+ 'Source code inclusion level: "full" (complete source), "summary" (file list with stats only), "none" (metadata only). Default: summary'
288
+ )
289
+ }),
290
+ async (args) => {
291
+ const clientManager = getClientManager()
292
+ const config = (clientManager as any).config
293
+
294
+ if (!config.etherscanApiKey) {
295
+ throw new Error(
296
+ 'Etherscan API key is required. Use --etherscan-api-key or set ETHERSCAN_API_KEY environment variable.'
297
+ )
298
+ }
299
+
300
+ if (!isAddress(args.address)) {
301
+ throw new Error('Invalid contract address')
302
+ }
303
+
304
+ const chains = CHAINS(config)
305
+ const chainId = chains[args.chain as ChainName].chain.id
306
+
307
+ try {
308
+ // Fetch main contract source using shared helper
309
+ const contractInfo = await fetchContractInfo(chainId, args.address, config.etherscanApiKey)
310
+
311
+ if (!contractInfo) {
312
+ throw new Error('No contract information found')
313
+ }
314
+
315
+ const isVerified = contractInfo.SourceCode !== ''
316
+ const isProxy = contractInfo.Proxy === '1'
317
+
318
+ if (!isVerified) {
319
+ return formatResponse({
320
+ success: false,
321
+ chain: args.chain,
322
+ chainId,
323
+ address: args.address,
324
+ isVerified: false,
325
+ message: 'Contract source code is not verified on Etherscan',
326
+ etherscanUrl: `https://${clientManager.getEtherscanDomain(args.chain as ChainName)}/address/${args.address}`
327
+ })
328
+ }
329
+
330
+ // Parse source code
331
+ let sourceFiles: Record<string, string> = {}
332
+ let sourceCodeString = contractInfo.SourceCode
333
+
334
+ // Handle multi-file sources (wrapped in {{ }} or [ ])
335
+ if (sourceCodeString.startsWith('{{') || sourceCodeString.startsWith('[')) {
336
+ try {
337
+ // Remove outer braces/brackets if present
338
+ if (sourceCodeString.startsWith('{{')) {
339
+ sourceCodeString = sourceCodeString.slice(1, -1)
340
+ }
341
+ const parsed = JSON.parse(sourceCodeString)
342
+
343
+ if (parsed.sources) {
344
+ // Standard JSON format
345
+ sourceFiles = Object.fromEntries(
346
+ Object.entries(parsed.sources).map(([path, data]: [string, any]) => [path, data.content || ''])
347
+ )
348
+ } else if (typeof parsed === 'object') {
349
+ // Simple object format
350
+ sourceFiles = parsed
351
+ }
352
+ } catch {
353
+ // If parsing fails, treat as single file
354
+ sourceFiles = { [contractInfo.ContractName + '.sol']: sourceCodeString }
355
+ }
356
+ } else {
357
+ // Single file source
358
+ sourceFiles = { [contractInfo.ContractName + '.sol']: sourceCodeString }
359
+ }
360
+
361
+ // Calculate file stats for summary mode
362
+ const fileStats = Object.entries(sourceFiles).map(([path, content]) => ({
363
+ path,
364
+ lines: (content as string).split('\n').length,
365
+ size: (content as string).length
366
+ }))
367
+
368
+ const result: Record<string, unknown> = {
369
+ success: true,
370
+ chain: args.chain,
371
+ chainId,
372
+ address: args.address,
373
+ isVerified: true,
374
+ isProxy,
375
+ contractName: contractInfo.ContractName,
376
+ compilerVersion: contractInfo.CompilerVersion,
377
+ optimizationUsed: contractInfo.OptimizationUsed === '1',
378
+ optimizationRuns: parseInt(contractInfo.Runs) || 0,
379
+ evmVersion: contractInfo.EVMVersion,
380
+ licenseType: contractInfo.LicenseType,
381
+ constructorArguments: contractInfo.ConstructorArguments,
382
+ fileCount: Object.keys(sourceFiles).length,
383
+ totalLines: fileStats.reduce((sum, stat) => sum + stat.lines, 0),
384
+ etherscanUrl: `https://${clientManager.getEtherscanDomain(args.chain as ChainName)}/address/${args.address}#code`
385
+ }
386
+
387
+ // Add source based on includeSource parameter
388
+ if (args.includeSource === 'full') {
389
+ result.sourceFiles = sourceFiles
390
+ } else if (args.includeSource === 'summary') {
391
+ result.files = fileStats
392
+ }
393
+ // 'none' mode: no source files added
394
+
395
+ // If proxy and implementation requested, fetch implementation source
396
+ if (
397
+ isProxy &&
398
+ args.includeImplementation &&
399
+ contractInfo.Implementation &&
400
+ isAddress(contractInfo.Implementation)
401
+ ) {
402
+ try {
403
+ const implInfo = await fetchContractInfo(chainId, contractInfo.Implementation, config.etherscanApiKey)
404
+
405
+ if (implInfo && implInfo.SourceCode !== '') {
406
+ // Parse implementation source code
407
+ let implSourceFiles: Record<string, string> = {}
408
+ let implSourceCodeString = implInfo.SourceCode
409
+
410
+ if (implSourceCodeString.startsWith('{{') || implSourceCodeString.startsWith('[')) {
411
+ try {
412
+ if (implSourceCodeString.startsWith('{{')) {
413
+ implSourceCodeString = implSourceCodeString.slice(1, -1)
414
+ }
415
+ const parsed = JSON.parse(implSourceCodeString)
416
+
417
+ if (parsed.sources) {
418
+ implSourceFiles = Object.fromEntries(
419
+ Object.entries(parsed.sources).map(([path, data]: [string, any]) => [path, data.content || ''])
420
+ )
421
+ } else if (typeof parsed === 'object') {
422
+ implSourceFiles = parsed
423
+ }
424
+ } catch {
425
+ implSourceFiles = { [implInfo.ContractName + '.sol']: implSourceCodeString }
426
+ }
427
+ } else {
428
+ implSourceFiles = { [implInfo.ContractName + '.sol']: implSourceCodeString }
429
+ }
430
+
431
+ // Calculate implementation file stats
432
+ const implFileStats = Object.entries(implSourceFiles).map(([path, content]) => ({
433
+ path,
434
+ lines: (content as string).split('\n').length,
435
+ size: (content as string).length
436
+ }))
437
+
438
+ const implResult: Record<string, unknown> = {
439
+ address: contractInfo.Implementation,
440
+ contractName: implInfo.ContractName,
441
+ compilerVersion: implInfo.CompilerVersion,
442
+ optimizationUsed: implInfo.OptimizationUsed === '1',
443
+ optimizationRuns: parseInt(implInfo.Runs) || 0,
444
+ evmVersion: implInfo.EVMVersion,
445
+ licenseType: implInfo.LicenseType,
446
+ fileCount: Object.keys(implSourceFiles).length,
447
+ totalLines: implFileStats.reduce((sum, stat) => sum + stat.lines, 0),
448
+ etherscanUrl: `https://${clientManager.getEtherscanDomain(args.chain as ChainName)}/address/${contractInfo.Implementation}#code`
449
+ }
450
+
451
+ // Add implementation source based on includeSource parameter
452
+ if (args.includeSource === 'full') {
453
+ implResult.sourceFiles = implSourceFiles
454
+ } else if (args.includeSource === 'summary') {
455
+ implResult.files = implFileStats
456
+ }
457
+
458
+ result.implementation = implResult
459
+
460
+ // Update cache with implementation source (rawInfo already cached by fetchContractInfo)
461
+ if (contractInfo.Implementation) {
462
+ const implCacheKey = getCacheKey(chainId, contractInfo.Implementation)
463
+ const existingImpl = contractCache.get(implCacheKey)!
464
+ contractCache.set(implCacheKey, {
465
+ ...existingImpl,
466
+ sourceFiles: implSourceFiles
467
+ })
468
+ }
469
+ }
470
+ } catch (error) {
471
+ // Implementation fetch failed, continue without it
472
+ result.implementationError = error instanceof Error ? error.message : 'Failed to fetch implementation source'
473
+ }
474
+ } else if (isProxy && contractInfo.Implementation) {
475
+ result.implementationAddress = contractInfo.Implementation
476
+ }
477
+
478
+ // Update cache with main contract source (rawInfo already cached by fetchContractInfo)
479
+ const cacheKey = getCacheKey(chainId, args.address)
480
+ const existing = contractCache.get(cacheKey)!
481
+ contractCache.set(cacheKey, {
482
+ ...existing,
483
+ sourceFiles
484
+ })
485
+
486
+ return formatResponse(result)
487
+ } catch (error) {
488
+ throw new Error(`Failed to fetch source code: ${error}`)
489
+ }
490
+ }
491
+ ),
492
+
493
+ get_contract_source_file: createTool(
494
+ 'Get Specific Contract Source File',
495
+ 'Retrieve a specific source file from a cached contract. Use get_contract_source_code first to cache the contract.',
496
+ z.object({
497
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
498
+ address: z.string().describe('The contract address'),
499
+ filePath: z.string().optional().describe('Specific file path to retrieve. If omitted, returns list of available files.'),
500
+ useImplementation: z
501
+ .boolean()
502
+ .optional()
503
+ .default(false)
504
+ .describe('For proxy contracts, get file from implementation instead (default: false)')
505
+ }),
506
+ async (args) => {
507
+ const clientManager = getClientManager()
508
+ const chains = CHAINS((clientManager as any).config)
509
+ const chainId = chains[args.chain as ChainName].chain.id
510
+
511
+ if (!isAddress(args.address)) {
512
+ throw new Error('Invalid contract address')
513
+ }
514
+
515
+ // Check cache
516
+ let cacheKey = getCacheKey(chainId, args.address)
517
+ let cached = contractCache.get(cacheKey)
518
+
519
+ // If useImplementation is true and we have implementation address, use that instead
520
+ if (args.useImplementation && cached?.metadata.implementationAddress) {
521
+ cacheKey = getCacheKey(chainId, cached.metadata.implementationAddress)
522
+ cached = contractCache.get(cacheKey)
523
+ }
524
+
525
+ if (!cached) {
526
+ return formatResponse({
527
+ success: false,
528
+ chain: args.chain,
529
+ chainId,
530
+ address: args.address,
531
+ message:
532
+ 'Contract not in cache. Use get_contract_source_code with includeSource="full" first to cache the contract.'
533
+ })
534
+ }
535
+
536
+ // If no filePath specified, return list of available files
537
+ if (!args.filePath) {
538
+ const fileList = Object.keys(cached.sourceFiles).map((path) => ({
539
+ path,
540
+ lines: cached.sourceFiles[path].split('\n').length,
541
+ size: cached.sourceFiles[path].length
542
+ }))
543
+
544
+ return formatResponse({
545
+ success: true,
546
+ chain: args.chain,
547
+ chainId,
548
+ address: args.address,
549
+ contractName: cached.metadata.contractName,
550
+ fileCount: fileList.length,
551
+ files: fileList
552
+ })
553
+ }
554
+
555
+ // Return specific file
556
+ const fileContent = cached.sourceFiles[args.filePath]
557
+ if (!fileContent) {
558
+ const availableFiles = Object.keys(cached.sourceFiles)
559
+ return formatResponse({
560
+ success: false,
561
+ chain: args.chain,
562
+ chainId,
563
+ address: args.address,
564
+ message: `File "${args.filePath}" not found in cached source`,
565
+ availableFiles
566
+ })
567
+ }
568
+
569
+ return formatResponse({
570
+ success: true,
571
+ chain: args.chain,
572
+ chainId,
573
+ address: args.address,
574
+ contractName: cached.metadata.contractName,
575
+ filePath: args.filePath,
576
+ content: fileContent,
577
+ lines: fileContent.split('\n').length,
578
+ size: fileContent.length
579
+ })
580
+ }
581
+ )
582
+ }