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.
- package/CHANGELOG.md +86 -2
- package/README.md +76 -52
- package/dist/chain-meta.d.ts.map +1 -1
- package/dist/chain-meta.js +19 -8
- package/dist/chain-meta.js.map +1 -1
- package/dist/clear-signing.d.ts.map +1 -1
- package/dist/clear-signing.js +17 -6
- package/dist/clear-signing.js.map +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +9 -3
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/kv-storage.d.ts +2 -1
- package/dist/kv-storage.d.ts.map +1 -1
- package/dist/kv-storage.js +5 -2
- package/dist/kv-storage.js.map +1 -1
- package/dist/oauth.d.ts +18 -4
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +62 -26
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +7 -5
- package/dist/preview.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +14 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read/advanced.d.ts +9 -4
- package/dist/tools/read/advanced.d.ts.map +1 -1
- package/dist/tools/read/advanced.js +45 -33
- package/dist/tools/read/advanced.js.map +1 -1
- package/dist/tools/read/balance.d.ts.map +1 -1
- package/dist/tools/read/balance.js +17 -32
- package/dist/tools/read/balance.js.map +1 -1
- package/dist/tools/read/contract-info.d.ts +2 -0
- package/dist/tools/read/contract-info.d.ts.map +1 -1
- package/dist/tools/read/contract-info.js +53 -122
- package/dist/tools/read/contract-info.js.map +1 -1
- package/dist/tools/read/contract.d.ts.map +1 -1
- package/dist/tools/read/contract.js +2 -1
- package/dist/tools/read/contract.js.map +1 -1
- package/dist/tools/read/ens.d.ts +20 -40
- package/dist/tools/read/ens.d.ts.map +1 -1
- package/dist/tools/read/ens.js +25 -33
- package/dist/tools/read/ens.js.map +1 -1
- package/dist/tools/read/gas.d.ts.map +1 -1
- package/dist/tools/read/gas.js +13 -48
- package/dist/tools/read/gas.js.map +1 -1
- package/dist/tools/read/logs.d.ts +61 -0
- package/dist/tools/read/logs.d.ts.map +1 -1
- package/dist/tools/read/logs.js +96 -115
- package/dist/tools/read/logs.js.map +1 -1
- package/dist/tools/read/signatures.d.ts.map +1 -1
- package/dist/tools/read/signatures.js +4 -3
- package/dist/tools/read/signatures.js.map +1 -1
- package/dist/tools/read/signing.d.ts.map +1 -1
- package/dist/tools/read/signing.js +8 -2
- package/dist/tools/read/signing.js.map +1 -1
- package/dist/tools/sign/transactions.d.ts +4 -4
- package/dist/tools/sign/transactions.d.ts.map +1 -1
- package/dist/tools/sign/transactions.js +54 -9
- package/dist/tools/sign/transactions.js.map +1 -1
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +43 -1
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +1 -0
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +16 -3
- package/dist/wallet-client.js.map +1 -1
- package/package.json +4 -7
- package/src/chain-meta.ts +16 -7
- package/src/clear-signing.ts +15 -9
- package/src/http-server.ts +8 -3
- package/src/index.ts +5 -1
- package/src/kv-storage.ts +6 -3
- package/src/oauth.ts +93 -34
- package/src/preview.ts +7 -5
- package/src/tools/index.ts +12 -1
- package/src/tools/read/advanced.ts +63 -34
- package/src/tools/read/balance.ts +18 -33
- package/src/tools/read/contract-info.ts +64 -144
- package/src/tools/read/contract.ts +2 -1
- package/src/tools/read/ens.ts +26 -33
- package/src/tools/read/gas.ts +13 -50
- package/src/tools/read/logs.ts +131 -143
- package/src/tools/read/signatures.ts +4 -4
- package/src/tools/read/signing.ts +8 -2
- package/src/tools/sign/transactions.ts +60 -9
- package/src/utils.ts +47 -1
- 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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
47
|
-
async function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
|
|
320
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
391
|
-
|
|
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) => {
|
package/src/tools/read/ens.ts
CHANGED
|
@@ -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
|
|
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:
|
|
164
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
351
|
-
|
|
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
|
|
package/src/tools/read/gas.ts
CHANGED
|
@@ -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
|
|
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:
|
|
239
|
-
}
|
|
221
|
+
blockNumber: args.blockNumber ? BigInt(args.blockNumber) : undefined
|
|
222
|
+
}
|
|
240
223
|
|
|
241
|
-
|
|
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:
|
|
283
|
-
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(
|
|
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
|
-
? `${(
|
|
436
|
+
? `${formatEther(estimatedCost)} ${client.chain?.nativeCurrency.symbol ?? 'ETH'}`
|
|
474
437
|
: estimatedCost.toString()
|
|
475
438
|
}
|
|
476
439
|
|