web3-tools-mcp 2.0.2 → 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 +182 -1
- package/README.md +76 -47
- package/dist/anvil.d.ts +17 -19
- package/dist/anvil.d.ts.map +1 -1
- package/dist/anvil.js +124 -228
- package/dist/anvil.js.map +1 -1
- 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/chains.d.ts +9 -1
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -2
- package/dist/chains.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/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +24 -0
- package/dist/context.js.map +1 -0
- package/dist/hosted.d.ts +12 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/hosted.js +12 -0
- package/dist/hosted.js.map +1 -0
- package/dist/http-server.d.ts +13 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +22 -7
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +20 -12
- 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/lib.d.ts +24 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +23 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +43 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/oauth.d.ts +57 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +133 -63
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts +7 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +21 -7
- package/dist/preview.js.map +1 -1
- package/dist/signing-requests.d.ts +81 -0
- package/dist/signing-requests.d.ts.map +1 -0
- package/dist/signing-requests.js +138 -0
- package/dist/signing-requests.js.map +1 -0
- package/dist/tools/index.d.ts +27 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +50 -15
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/{advanced.d.ts → read/advanced.d.ts} +39 -30
- package/dist/tools/read/advanced.d.ts.map +1 -0
- package/dist/tools/{advanced.js → read/advanced.js} +53 -41
- package/dist/tools/read/advanced.js.map +1 -0
- package/dist/tools/{balance.d.ts → read/balance.d.ts} +8 -7
- package/dist/tools/read/balance.d.ts.map +1 -0
- package/dist/tools/{balance.js → read/balance.js} +18 -33
- package/dist/tools/read/balance.js.map +1 -0
- package/dist/tools/{contract-info.d.ts → read/contract-info.d.ts} +20 -15
- package/dist/tools/read/contract-info.d.ts.map +1 -0
- package/dist/tools/{contract-info.js → read/contract-info.js} +54 -123
- package/dist/tools/read/contract-info.js.map +1 -0
- package/dist/tools/{contract.d.ts → read/contract.d.ts} +14 -12
- package/dist/tools/read/contract.d.ts.map +1 -0
- package/dist/tools/{contract.js → read/contract.js} +3 -2
- package/dist/tools/read/contract.js.map +1 -0
- package/dist/tools/read/ens.d.ts +104 -0
- package/dist/tools/read/ens.d.ts.map +1 -0
- package/dist/tools/{ens.js → read/ens.js} +25 -33
- package/dist/tools/read/ens.js.map +1 -0
- package/dist/tools/read/gas.d.ts +171 -0
- package/dist/tools/read/gas.d.ts.map +1 -0
- package/dist/tools/read/gas.js +333 -0
- package/dist/tools/read/gas.js.map +1 -0
- package/dist/tools/read/logs.d.ts +101 -0
- package/dist/tools/read/logs.d.ts.map +1 -0
- package/dist/tools/read/logs.js +211 -0
- package/dist/tools/read/logs.js.map +1 -0
- package/dist/tools/{signatures.d.ts → read/signatures.d.ts} +8 -4
- package/dist/tools/read/signatures.d.ts.map +1 -0
- package/dist/tools/{signatures.js → read/signatures.js} +4 -3
- package/dist/tools/read/signatures.js.map +1 -0
- package/dist/tools/read/signing.d.ts +27 -0
- package/dist/tools/read/signing.d.ts.map +1 -0
- package/dist/tools/read/signing.js +81 -0
- package/dist/tools/read/signing.js.map +1 -0
- package/dist/tools/sign/errors.d.ts +9 -0
- package/dist/tools/sign/errors.d.ts.map +1 -0
- package/dist/tools/sign/errors.js +29 -0
- package/dist/tools/sign/errors.js.map +1 -0
- package/dist/tools/sign/transactions.d.ts +141 -0
- package/dist/tools/sign/transactions.d.ts.map +1 -0
- package/dist/tools/sign/transactions.js +361 -0
- package/dist/tools/sign/transactions.js.map +1 -0
- package/dist/tools/sign/wallet.d.ts +50 -0
- package/dist/tools/sign/wallet.d.ts.map +1 -0
- package/dist/tools/sign/wallet.js +180 -0
- package/dist/tools/sign/wallet.js.map +1 -0
- package/dist/utils.d.ts +23 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +48 -3
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +34 -4
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +106 -27
- package/dist/wallet-client.js.map +1 -1
- package/dist/walletconnect.d.ts +100 -13
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js +215 -50
- package/dist/walletconnect.js.map +1 -1
- package/package.json +12 -9
- package/src/anvil.ts +150 -271
- package/src/chain-meta.ts +16 -7
- package/src/chains.ts +14 -2
- package/src/clear-signing.ts +15 -9
- package/src/client.ts +0 -1
- package/src/context.ts +27 -0
- package/src/hosted.ts +11 -0
- package/src/http-server.ts +38 -8
- package/src/index.ts +20 -13
- package/src/kv-storage.ts +6 -3
- package/src/lib.ts +24 -0
- package/src/mcp-server.ts +47 -0
- package/src/oauth.ts +181 -69
- package/src/preview.ts +28 -7
- package/src/signing-requests.ts +198 -0
- package/src/tools/index.ts +78 -16
- package/src/tools/{advanced.ts → read/advanced.ts} +74 -43
- package/src/tools/{balance.ts → read/balance.ts} +20 -35
- package/src/tools/{contract-info.ts → read/contract-info.ts} +66 -146
- package/src/tools/{contract.ts → read/contract.ts} +4 -3
- package/src/tools/{ens.ts → read/ens.ts} +27 -34
- package/src/tools/read/gas.ts +443 -0
- package/src/tools/read/logs.ts +281 -0
- package/src/tools/{signatures.ts → read/signatures.ts} +5 -5
- package/src/tools/read/signing.ts +92 -0
- package/src/tools/sign/errors.ts +28 -0
- package/src/tools/sign/transactions.ts +479 -0
- package/src/tools/sign/wallet.ts +216 -0
- package/src/utils.ts +52 -3
- package/src/wallet-client.ts +107 -24
- package/src/walletconnect.ts +250 -48
- package/dist/tools/advanced.d.ts.map +0 -1
- package/dist/tools/advanced.js.map +0 -1
- package/dist/tools/balance.d.ts.map +0 -1
- package/dist/tools/balance.js.map +0 -1
- package/dist/tools/contract-info.d.ts.map +0 -1
- package/dist/tools/contract-info.js.map +0 -1
- package/dist/tools/contract.d.ts.map +0 -1
- package/dist/tools/contract.js.map +0 -1
- package/dist/tools/ens.d.ts +0 -119
- package/dist/tools/ens.d.ts.map +0 -1
- package/dist/tools/ens.js.map +0 -1
- package/dist/tools/gas.d.ts +0 -99
- package/dist/tools/gas.d.ts.map +0 -1
- package/dist/tools/gas.js +0 -181
- package/dist/tools/gas.js.map +0 -1
- package/dist/tools/logs.d.ts +0 -39
- package/dist/tools/logs.d.ts.map +0 -1
- package/dist/tools/logs.js +0 -230
- package/dist/tools/logs.js.map +0 -1
- package/dist/tools/signatures.d.ts.map +0 -1
- package/dist/tools/signatures.js.map +0 -1
- package/dist/tools/transactions.d.ts +0 -155
- package/dist/tools/transactions.d.ts.map +0 -1
- package/dist/tools/transactions.js +0 -365
- package/dist/tools/transactions.js.map +0 -1
- package/src/tools/gas.ts +0 -219
- package/src/tools/logs.ts +0 -293
- package/src/tools/transactions.ts +0 -456
package/src/tools/index.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'
|
|
3
3
|
import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
import { getClientManager } from '../client.js'
|
|
5
|
+
import { identityFrom } from '../context.js'
|
|
6
|
+
import type { ToolResult } from '../types.js'
|
|
7
|
+
import { redactSecrets } from '../utils.js'
|
|
8
|
+
import advancedTools from './read/advanced.js'
|
|
9
|
+
import balanceTools from './read/balance.js'
|
|
10
|
+
import contractTools from './read/contract.js'
|
|
11
|
+
import contractInfoTools from './read/contract-info.js'
|
|
12
|
+
import ensTools from './read/ens.js'
|
|
13
|
+
import gasTools from './read/gas.js'
|
|
14
|
+
import logTools from './read/logs.js'
|
|
15
|
+
import signatureTools from './read/signatures.js'
|
|
16
|
+
import signingRequestTools from './read/signing.js'
|
|
17
|
+
import transactionTools from './sign/transactions.js'
|
|
18
|
+
import walletTools from './sign/wallet.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What a tool does with a wallet, which is the line everything else divides along.
|
|
22
|
+
*
|
|
23
|
+
* `read` answers questions about a chain and costs an RPC call. `sign` puts a transaction
|
|
24
|
+
* in front of somebody's wallet, and is the reason identity and rooms exist. A host pricing
|
|
25
|
+
* or rate-limiting tool calls almost always wants this distinction, and taking it from the
|
|
26
|
+
* folder a tool lives in means it cannot drift from a hand-kept list.
|
|
27
|
+
*/
|
|
28
|
+
export type ToolGroup = 'read' | 'sign'
|
|
29
|
+
|
|
30
|
+
const readTools = {
|
|
15
31
|
...signatureTools,
|
|
16
32
|
...contractTools,
|
|
17
33
|
...contractInfoTools,
|
|
@@ -20,12 +36,45 @@ const allToolDefinitions = {
|
|
|
20
36
|
...advancedTools,
|
|
21
37
|
...ensTools,
|
|
22
38
|
...gasTools,
|
|
23
|
-
|
|
39
|
+
// A signature already asked for and paid for; collecting it is not a second request.
|
|
40
|
+
...signingRequestTools
|
|
24
41
|
} as const
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
const signTools = {
|
|
44
|
+
...transactionTools,
|
|
45
|
+
...walletTools
|
|
46
|
+
} as const
|
|
47
|
+
|
|
48
|
+
const allToolDefinitions = { ...readTools, ...signTools } as const
|
|
49
|
+
|
|
50
|
+
const groups = new Map<string, ToolGroup>([
|
|
51
|
+
...Object.keys(readTools).map((name) => [name, 'read'] as const),
|
|
52
|
+
...Object.keys(signTools).map((name) => [name, 'sign'] as const)
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
/** The group a tool belongs to, for a host deciding what it costs or who may call it. */
|
|
56
|
+
export function toolGroup(name: string): ToolGroup | undefined {
|
|
57
|
+
return groups.get(name)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wraps every tool call, for a host that needs to say who may run what.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately only the tool's name, its group and who is calling: enough to count calls,
|
|
64
|
+
* refuse them past a limit and record what was used, without this package taking a view on
|
|
65
|
+
* any of it. Throwing refuses the call, and the message reaches the agent — so it should
|
|
66
|
+
* say what to do about it. Whatever the returned promise settles as is what the caller
|
|
67
|
+
* sees, so a host can also time a call or meter it once it has succeeded.
|
|
68
|
+
*/
|
|
69
|
+
export type ToolMiddleware = (
|
|
70
|
+
call: { tool: string; group: ToolGroup; identity: string },
|
|
71
|
+
run: () => Promise<ToolResult>
|
|
72
|
+
) => Promise<ToolResult>
|
|
73
|
+
|
|
74
|
+
export function registerAllTools(server: McpServer, middleware?: ToolMiddleware) {
|
|
28
75
|
Object.entries(allToolDefinitions).forEach(([name, tool]) => {
|
|
76
|
+
if (tool.available && !tool.available()) return
|
|
77
|
+
|
|
29
78
|
server.registerTool(
|
|
30
79
|
name,
|
|
31
80
|
{
|
|
@@ -33,8 +82,21 @@ export function registerAllTools(server: McpServer) {
|
|
|
33
82
|
description: tool.description,
|
|
34
83
|
inputSchema: tool.schema.shape
|
|
35
84
|
},
|
|
36
|
-
async (args: any,
|
|
37
|
-
|
|
85
|
+
async (args: any, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
|
|
86
|
+
// Passed to every handler; the ones that reach a signer must say whose it is.
|
|
87
|
+
const identity = identityFrom(extra.authInfo)
|
|
88
|
+
const run = () => tool.handler(args, identity)
|
|
89
|
+
const group = toolGroup(name) as ToolGroup
|
|
90
|
+
// Anything a tool returns or throws may quote an RPC URL, and those carry provider keys.
|
|
91
|
+
const config = getClientManager().getConfig()
|
|
92
|
+
try {
|
|
93
|
+
const result = await (middleware ? middleware({ tool: name, group, identity }, run) : run())
|
|
94
|
+
return { ...result, content: result.content.map((item) => ({ ...item, text: redactSecrets(item.text, config) })) }
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!(error instanceof Error)) throw new Error(redactSecrets(String(error), config))
|
|
97
|
+
error.message = redactSecrets(error.message, config)
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
38
100
|
}
|
|
39
101
|
)
|
|
40
102
|
})
|
|
@@ -1,9 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AbiFunction,
|
|
3
|
+
type Address,
|
|
4
|
+
decodeAbiParameters,
|
|
5
|
+
encodeFunctionData,
|
|
6
|
+
isAddress,
|
|
7
|
+
parseAbiItem,
|
|
8
|
+
parseAbiParameters
|
|
9
|
+
} from 'viem'
|
|
2
10
|
import { z } from 'zod'
|
|
3
|
-
import {
|
|
4
|
-
import { getClientManager, SUPPORTED_CHAINS } from '
|
|
5
|
-
import type { ChainName } from '
|
|
6
|
-
import { createTool, formatResponse, summarizeTrace } from '
|
|
11
|
+
import { isDebugNodeAvailable, simulateCallWithTrace, traceTransactionWithAnvil } from '../../anvil.js'
|
|
12
|
+
import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
|
|
13
|
+
import type { ChainName } from '../../types.js'
|
|
14
|
+
import { convertArgumentsToTypes, createTool, formatResponse, rpcReason, summarizeTrace } from '../../utils.js'
|
|
15
|
+
|
|
16
|
+
/** Past these, a trace would crowd everything else out of the agent's context. */
|
|
17
|
+
const MAX_TRACE_CHARS = 50_000
|
|
18
|
+
const MAX_INPUT_CHARS = 10_000
|
|
19
|
+
const MAX_RECEIPT_LOGS = 100
|
|
20
|
+
|
|
21
|
+
const SUMMARIZE_DESCRIPTION =
|
|
22
|
+
'Return a compact summary instead of the full trace: the path to the revert, or the top-level calls, with selectors only and no calldata.'
|
|
23
|
+
|
|
24
|
+
/** The trace itself if it fits, otherwise its JSON cut short. */
|
|
25
|
+
export function boundTrace(trace: unknown): { trace: unknown; truncated?: true } {
|
|
26
|
+
const json = JSON.stringify(trace) ?? ''
|
|
27
|
+
return json.length > MAX_TRACE_CHARS ? { trace: json.slice(0, MAX_TRACE_CHARS), truncated: true } : { trace }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TRACERS = {
|
|
31
|
+
trace: { tracer: 'callTracer' },
|
|
32
|
+
prestate: { tracer: 'prestateTracer' },
|
|
33
|
+
stateDiff: { tracer: 'prestateTracer', tracerConfig: { diffMode: true } }
|
|
34
|
+
} as const
|
|
7
35
|
|
|
8
36
|
export default {
|
|
9
37
|
get_storage_at: createTool(
|
|
@@ -77,8 +105,8 @@ export default {
|
|
|
77
105
|
const parsedTypes = parseAbiParameters([args.abiType])
|
|
78
106
|
if (parsedTypes.length > 0) {
|
|
79
107
|
const decoded = decodeAbiParameters(parsedTypes, storageValue) as readonly unknown[]
|
|
80
|
-
decodedValue = decoded
|
|
81
|
-
formattedValue = decodedValue
|
|
108
|
+
decodedValue = decoded[0]
|
|
109
|
+
formattedValue = decodedValue === undefined ? 'No data' : String(decodedValue)
|
|
82
110
|
} else {
|
|
83
111
|
decodedValue = null
|
|
84
112
|
formattedValue = 'Invalid ABI type'
|
|
@@ -148,20 +176,20 @@ export default {
|
|
|
148
176
|
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
|
|
149
177
|
transactionHash: z.string().describe('Transaction hash to trace (0x prefixed)'),
|
|
150
178
|
traceType: z
|
|
151
|
-
.enum(['trace', '
|
|
179
|
+
.enum(['trace', 'prestate', 'stateDiff'])
|
|
152
180
|
.optional()
|
|
153
|
-
.describe(
|
|
181
|
+
.describe(
|
|
182
|
+
'Trace type: "trace" (call tree, recommended), "prestate" (state of every touched account before the tx), "stateDiff" (state before and after, changed fields only)'
|
|
183
|
+
)
|
|
154
184
|
.default('trace'),
|
|
155
185
|
useAnvil: z
|
|
156
186
|
.boolean()
|
|
157
187
|
.optional()
|
|
158
|
-
.describe(
|
|
188
|
+
.describe(
|
|
189
|
+
'Force tracing through the debug node (ANVIL_RPC_URL). Used automatically as a fallback when the RPC cannot trace.'
|
|
190
|
+
)
|
|
159
191
|
.default(false),
|
|
160
|
-
summarize: z
|
|
161
|
-
.boolean()
|
|
162
|
-
.optional()
|
|
163
|
-
.describe('Return a compact summary instead of full trace. Truncates hex data and flattens nested calls.')
|
|
164
|
-
.default(false)
|
|
192
|
+
summarize: z.boolean().optional().describe(SUMMARIZE_DESCRIPTION).default(false)
|
|
165
193
|
}),
|
|
166
194
|
async (args) => {
|
|
167
195
|
const clientManager = getClientManager()
|
|
@@ -174,24 +202,18 @@ export default {
|
|
|
174
202
|
let traceResult: unknown = null
|
|
175
203
|
let usedAnvil = false
|
|
176
204
|
|
|
177
|
-
|
|
178
|
-
const tracerMap: Record<string, 'callTracer' | 'prestateTracer' | 'stateDiffTracer'> = {
|
|
179
|
-
trace: 'callTracer',
|
|
180
|
-
vmTrace: 'prestateTracer',
|
|
181
|
-
stateDiff: 'stateDiffTracer'
|
|
182
|
-
}
|
|
183
|
-
const tracer = tracerMap[args.traceType ?? 'trace'] ?? 'callTracer'
|
|
205
|
+
const tracerOptions = TRACERS[args.traceType ?? 'trace']
|
|
184
206
|
|
|
185
207
|
// Try RPC tracing first (unless forceAnvil is true)
|
|
186
208
|
if (!args.useAnvil) {
|
|
187
209
|
try {
|
|
188
210
|
traceResult = await client.request({
|
|
189
211
|
method: 'debug_traceTransaction',
|
|
190
|
-
params: [args.transactionHash,
|
|
212
|
+
params: [args.transactionHash, tracerOptions]
|
|
191
213
|
})
|
|
192
214
|
} catch (rpcError) {
|
|
193
215
|
// RPC tracing failed, will try Anvil fallback
|
|
194
|
-
const errorMessage = (rpcError
|
|
216
|
+
const errorMessage = rpcReason(rpcError)
|
|
195
217
|
if (
|
|
196
218
|
errorMessage.includes('not supported') ||
|
|
197
219
|
errorMessage.includes('not available') ||
|
|
@@ -213,18 +235,18 @@ export default {
|
|
|
213
235
|
(traceResult && typeof traceResult === 'object' && 'rpcError' in traceResult) ||
|
|
214
236
|
args.useAnvil
|
|
215
237
|
) {
|
|
216
|
-
const anvilAvailable = await
|
|
238
|
+
const anvilAvailable = await isDebugNodeAvailable()
|
|
217
239
|
if (anvilAvailable) {
|
|
218
240
|
usedAnvil = true // Mark as used before attempting (even if it fails)
|
|
219
241
|
try {
|
|
220
242
|
const forkUrl = clientManager.getRpcUrl(args.chain as ChainName)
|
|
221
243
|
const blockNumber = transaction.blockNumber ?? 0n
|
|
222
244
|
|
|
223
|
-
traceResult = await traceTransactionWithAnvil(forkUrl, args.transactionHash, blockNumber, tracer
|
|
245
|
+
traceResult = await traceTransactionWithAnvil(forkUrl, args.transactionHash, blockNumber, tracerOptions.tracer)
|
|
224
246
|
} catch (anvilError) {
|
|
225
247
|
// Anvil tracing also failed
|
|
226
248
|
traceResult = {
|
|
227
|
-
error: `Anvil tracing failed: ${(anvilError
|
|
249
|
+
error: `Anvil tracing failed: ${rpcReason(anvilError)}`,
|
|
228
250
|
rpcError:
|
|
229
251
|
traceResult && typeof traceResult === 'object' && 'rpcError' in traceResult
|
|
230
252
|
? (traceResult as { rpcError: string }).rpcError
|
|
@@ -235,7 +257,7 @@ export default {
|
|
|
235
257
|
// Anvil not available and RPC failed
|
|
236
258
|
traceResult = {
|
|
237
259
|
error:
|
|
238
|
-
'Tracing not available.
|
|
260
|
+
'Tracing not available. Point ANVIL_RPC_URL at a node exposing debug_traceCall — a local Anvil on 127.0.0.1:8545 is picked up automatically.',
|
|
239
261
|
rpcError:
|
|
240
262
|
traceResult && typeof traceResult === 'object' && 'rpcError' in traceResult
|
|
241
263
|
? (traceResult as { rpcError: string }).rpcError
|
|
@@ -255,11 +277,20 @@ export default {
|
|
|
255
277
|
const traceSummary =
|
|
256
278
|
args.summarize && traceResult && typeof traceResult === 'object' && !isPlainError ? summarizeTrace(traceResult) : null
|
|
257
279
|
|
|
280
|
+
const replayNote =
|
|
281
|
+
usedAnvil && !isPlainError
|
|
282
|
+
? 'Replayed on the debug node against the state at the end of the previous block: transactions before this one in the same block were not applied, so the trace can differ from what happened on chain.' +
|
|
283
|
+
(args.traceType === 'stateDiff' ? ' This replay shows the prestate only, not a diff.' : '')
|
|
284
|
+
: undefined
|
|
285
|
+
const bounded = boundTrace(traceResult)
|
|
286
|
+
const input = transaction.input ?? '0x'
|
|
287
|
+
|
|
258
288
|
const result =
|
|
259
289
|
args.summarize && traceSummary
|
|
260
290
|
? {
|
|
261
291
|
chain: args.chain,
|
|
262
292
|
transactionHash: args.transactionHash,
|
|
293
|
+
...(replayNote && { note: replayNote }),
|
|
263
294
|
status: receipt.status,
|
|
264
295
|
gasUsed: receipt.gasUsed?.toString(),
|
|
265
296
|
...traceSummary // { hasError, errorPath, summary }
|
|
@@ -270,6 +301,10 @@ export default {
|
|
|
270
301
|
transactionHash: args.transactionHash,
|
|
271
302
|
traceType: args.traceType,
|
|
272
303
|
usedAnvil,
|
|
304
|
+
...(replayNote && { note: replayNote }),
|
|
305
|
+
...((bounded.truncated || input.length > MAX_INPUT_CHARS || receipt.logs.length > MAX_RECEIPT_LOGS) && {
|
|
306
|
+
truncated: true
|
|
307
|
+
}),
|
|
273
308
|
transaction: {
|
|
274
309
|
blockNumber: transaction.blockNumber?.toString(),
|
|
275
310
|
from: transaction.from,
|
|
@@ -278,19 +313,20 @@ export default {
|
|
|
278
313
|
gas: transaction.gas?.toString() || '0',
|
|
279
314
|
gasPrice: transaction.gasPrice?.toString() || '0',
|
|
280
315
|
nonce: transaction.nonce?.toString() || '0',
|
|
281
|
-
input:
|
|
316
|
+
input: input.length > MAX_INPUT_CHARS ? `${input.slice(0, MAX_INPUT_CHARS)}…` : input
|
|
282
317
|
},
|
|
283
318
|
receipt: {
|
|
284
319
|
status: receipt.status,
|
|
285
320
|
gasUsed: receipt.gasUsed?.toString() || '0',
|
|
286
321
|
effectiveGasPrice: receipt.effectiveGasPrice?.toString() || '0',
|
|
287
|
-
|
|
322
|
+
logCount: receipt.logs.length,
|
|
323
|
+
logs: receipt.logs.slice(0, MAX_RECEIPT_LOGS).map((log) => ({
|
|
288
324
|
address: log.address,
|
|
289
325
|
topics: log.topics,
|
|
290
326
|
data: log.data
|
|
291
327
|
}))
|
|
292
328
|
},
|
|
293
|
-
trace:
|
|
329
|
+
trace: bounded.trace
|
|
294
330
|
}
|
|
295
331
|
|
|
296
332
|
return formatResponse(result)
|
|
@@ -299,7 +335,7 @@ export default {
|
|
|
299
335
|
|
|
300
336
|
debug_call: createTool(
|
|
301
337
|
'Debug Contract Call',
|
|
302
|
-
'Simulate a contract call with full trace output.
|
|
338
|
+
'Simulate a contract call with full trace output. Needs a node exposing debug_traceCall (a local Anvil, or ANVIL_RPC_URL). Useful for debugging reverts and understanding call execution.',
|
|
303
339
|
z.object({
|
|
304
340
|
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to fork'),
|
|
305
341
|
to: z.string().describe('Contract address to call'),
|
|
@@ -320,17 +356,13 @@ export default {
|
|
|
320
356
|
.optional()
|
|
321
357
|
.default('callTracer')
|
|
322
358
|
.describe('Trace type: callTracer (call tree) or prestateTracer (state before execution)'),
|
|
323
|
-
summarize: z
|
|
324
|
-
.boolean()
|
|
325
|
-
.optional()
|
|
326
|
-
.describe('Return a compact summary instead of full trace. Truncates hex data and flattens nested calls.')
|
|
327
|
-
.default(false)
|
|
359
|
+
summarize: z.boolean().optional().describe(SUMMARIZE_DESCRIPTION).default(false)
|
|
328
360
|
}),
|
|
329
361
|
async (args) => {
|
|
330
362
|
// Check if Anvil is installed
|
|
331
|
-
const anvilAvailable = await
|
|
363
|
+
const anvilAvailable = await isDebugNodeAvailable()
|
|
332
364
|
if (!anvilAvailable) {
|
|
333
|
-
throw new Error('
|
|
365
|
+
throw new Error('No tracing node reachable. Run a local Anvil, or set ANVIL_RPC_URL to a node exposing debug_traceCall.')
|
|
334
366
|
}
|
|
335
367
|
|
|
336
368
|
const clientManager = getClientManager()
|
|
@@ -340,15 +372,14 @@ export default {
|
|
|
340
372
|
let calldata = args.data
|
|
341
373
|
if (args.functionAbi && !calldata) {
|
|
342
374
|
try {
|
|
343
|
-
const
|
|
344
|
-
const abiItem = parseAbiItem(args.functionAbi)
|
|
375
|
+
const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
|
|
345
376
|
if (abiItem.type !== 'function') {
|
|
346
377
|
throw new Error('ABI must be a function signature')
|
|
347
378
|
}
|
|
348
379
|
calldata = encodeFunctionData({
|
|
349
380
|
abi: [abiItem],
|
|
350
381
|
functionName: abiItem.name,
|
|
351
|
-
args: (args.args ?? [])
|
|
382
|
+
args: convertArgumentsToTypes(args.args ?? [], abiItem.inputs)
|
|
352
383
|
})
|
|
353
384
|
} catch (encodeError) {
|
|
354
385
|
throw new Error(`Failed to encode function call: ${(encodeError as Error).message}`)
|
|
@@ -404,7 +435,7 @@ export default {
|
|
|
404
435
|
result: traceResult.result,
|
|
405
436
|
gasUsed: traceResult.gasUsed.toString(),
|
|
406
437
|
revertReason: traceResult.revertReason,
|
|
407
|
-
|
|
438
|
+
...boundTrace(traceResult.trace)
|
|
408
439
|
}
|
|
409
440
|
|
|
410
441
|
return formatResponse(result)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type Address, formatEther, formatUnits, isAddress } from 'viem'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
3
|
+
import { tokenMeta } from '../../chain-meta.js'
|
|
4
|
+
import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
|
|
5
|
+
import type { ChainName } from '../../types.js'
|
|
6
|
+
import { createTool, formatResponse, MAX_BATCH, rpcReason } from '../../utils.js'
|
|
6
7
|
|
|
7
8
|
const BalanceQuerySchema = z.object({
|
|
8
9
|
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
|
|
@@ -19,6 +20,7 @@ export default {
|
|
|
19
20
|
z.object({
|
|
20
21
|
queries: z
|
|
21
22
|
.array(BalanceQuerySchema)
|
|
23
|
+
.max(MAX_BATCH)
|
|
22
24
|
.describe('Array of balance queries. Omit tokenAddress for native balance, include for ERC20.')
|
|
23
25
|
}),
|
|
24
26
|
async (args) => {
|
|
@@ -42,7 +44,7 @@ export default {
|
|
|
42
44
|
success: boolean
|
|
43
45
|
balance?: unknown
|
|
44
46
|
balanceFormatted?: string
|
|
45
|
-
decimals?: number
|
|
47
|
+
decimals?: number | null
|
|
46
48
|
error?: string
|
|
47
49
|
}> = []
|
|
48
50
|
|
|
@@ -86,42 +88,24 @@ export default {
|
|
|
86
88
|
throw new Error(`Invalid token address format: ${query.tokenAddress}`)
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
const balance = await
|
|
90
|
-
|
|
91
|
-
abi: [
|
|
92
|
-
{
|
|
93
|
-
name: 'balanceOf',
|
|
94
|
-
type: 'function',
|
|
95
|
-
stateMutability: 'view',
|
|
96
|
-
inputs: [{ name: 'account', type: 'address' }],
|
|
97
|
-
outputs: [{ name: '', type: 'uint256' }]
|
|
98
|
-
}
|
|
99
|
-
],
|
|
100
|
-
functionName: 'balanceOf',
|
|
101
|
-
args: [query.address as Address],
|
|
102
|
-
blockNumber: blockTag === 'latest' ? undefined : blockTag
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
// Try to get decimals
|
|
106
|
-
let decimals = 18
|
|
107
|
-
try {
|
|
108
|
-
decimals = (await client.readContract({
|
|
91
|
+
const [balance, { decimals }] = await Promise.all([
|
|
92
|
+
client.readContract({
|
|
109
93
|
address: query.tokenAddress as Address,
|
|
110
94
|
abi: [
|
|
111
95
|
{
|
|
112
|
-
name: '
|
|
96
|
+
name: 'balanceOf',
|
|
113
97
|
type: 'function',
|
|
114
98
|
stateMutability: 'view',
|
|
115
|
-
inputs: [],
|
|
116
|
-
outputs: [{ name: '', type: '
|
|
99
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
100
|
+
outputs: [{ name: '', type: 'uint256' }]
|
|
117
101
|
}
|
|
118
102
|
],
|
|
119
|
-
functionName: '
|
|
103
|
+
functionName: 'balanceOf',
|
|
104
|
+
args: [query.address as Address],
|
|
120
105
|
blockNumber: blockTag === 'latest' ? undefined : blockTag
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
106
|
+
}),
|
|
107
|
+
tokenMeta(chain as ChainName, query.tokenAddress).catch(() => ({ decimals: undefined }))
|
|
108
|
+
])
|
|
125
109
|
|
|
126
110
|
return {
|
|
127
111
|
index: originalIndex,
|
|
@@ -132,8 +116,9 @@ export default {
|
|
|
132
116
|
blockNumber: query.blockNumber || 'latest',
|
|
133
117
|
success: true,
|
|
134
118
|
balance,
|
|
135
|
-
|
|
136
|
-
decimals
|
|
119
|
+
// Unknown decimals leave only the raw balance: guessing 18 could be off by 10^12.
|
|
120
|
+
balanceFormatted: decimals === undefined ? undefined : formatUnits(balance, decimals),
|
|
121
|
+
decimals: decimals ?? null
|
|
137
122
|
}
|
|
138
123
|
})
|
|
139
124
|
)
|
|
@@ -153,7 +138,7 @@ export default {
|
|
|
153
138
|
chain: query.chain,
|
|
154
139
|
blockNumber: query.blockNumber || 'latest',
|
|
155
140
|
success: false,
|
|
156
|
-
error:
|
|
141
|
+
error: rpcReason(result.reason)
|
|
157
142
|
}
|
|
158
143
|
}
|
|
159
144
|
})
|