web3-tools-mcp 2.0.2 → 3.0.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 (162) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -0
  3. package/dist/anvil.d.ts +17 -19
  4. package/dist/anvil.d.ts.map +1 -1
  5. package/dist/anvil.js +124 -228
  6. package/dist/anvil.js.map +1 -1
  7. package/dist/chains.d.ts +9 -1
  8. package/dist/chains.d.ts.map +1 -1
  9. package/dist/chains.js +11 -2
  10. package/dist/chains.js.map +1 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js.map +1 -1
  13. package/dist/context.d.ts +22 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +24 -0
  16. package/dist/context.js.map +1 -0
  17. package/dist/hosted.d.ts +12 -0
  18. package/dist/hosted.d.ts.map +1 -0
  19. package/dist/hosted.js +12 -0
  20. package/dist/hosted.js.map +1 -0
  21. package/dist/http-server.d.ts +13 -0
  22. package/dist/http-server.d.ts.map +1 -1
  23. package/dist/http-server.js +13 -4
  24. package/dist/http-server.js.map +1 -1
  25. package/dist/index.js +15 -11
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib.d.ts +24 -0
  28. package/dist/lib.d.ts.map +1 -0
  29. package/dist/lib.js +23 -0
  30. package/dist/lib.js.map +1 -0
  31. package/dist/mcp-server.d.ts +4 -0
  32. package/dist/mcp-server.d.ts.map +1 -0
  33. package/dist/mcp-server.js +43 -0
  34. package/dist/mcp-server.js.map +1 -0
  35. package/dist/oauth.d.ts +41 -6
  36. package/dist/oauth.d.ts.map +1 -1
  37. package/dist/oauth.js +75 -41
  38. package/dist/oauth.js.map +1 -1
  39. package/dist/preview.d.ts +7 -0
  40. package/dist/preview.d.ts.map +1 -1
  41. package/dist/preview.js +14 -2
  42. package/dist/preview.js.map +1 -1
  43. package/dist/signing-requests.d.ts +81 -0
  44. package/dist/signing-requests.d.ts.map +1 -0
  45. package/dist/signing-requests.js +138 -0
  46. package/dist/signing-requests.js.map +1 -0
  47. package/dist/tools/index.d.ts +27 -1
  48. package/dist/tools/index.d.ts.map +1 -1
  49. package/dist/tools/index.js +37 -15
  50. package/dist/tools/index.js.map +1 -1
  51. package/dist/tools/{advanced.d.ts → read/advanced.d.ts} +30 -26
  52. package/dist/tools/read/advanced.d.ts.map +1 -0
  53. package/dist/tools/{advanced.js → read/advanced.js} +10 -10
  54. package/dist/tools/read/advanced.js.map +1 -0
  55. package/dist/tools/{balance.d.ts → read/balance.d.ts} +8 -7
  56. package/dist/tools/read/balance.d.ts.map +1 -0
  57. package/dist/tools/{balance.js → read/balance.js} +2 -2
  58. package/dist/tools/read/balance.js.map +1 -0
  59. package/dist/tools/{contract-info.d.ts → read/contract-info.d.ts} +18 -15
  60. package/dist/tools/read/contract-info.d.ts.map +1 -0
  61. package/dist/tools/{contract-info.js → read/contract-info.js} +2 -2
  62. package/dist/tools/read/contract-info.js.map +1 -0
  63. package/dist/tools/{contract.d.ts → read/contract.d.ts} +14 -12
  64. package/dist/tools/read/contract.d.ts.map +1 -0
  65. package/dist/tools/{contract.js → read/contract.js} +2 -2
  66. package/dist/tools/read/contract.js.map +1 -0
  67. package/dist/tools/{ens.d.ts → read/ens.d.ts} +30 -25
  68. package/dist/tools/read/ens.d.ts.map +1 -0
  69. package/dist/tools/{ens.js → read/ens.js} +2 -2
  70. package/dist/tools/read/ens.js.map +1 -0
  71. package/dist/tools/read/gas.d.ts +171 -0
  72. package/dist/tools/read/gas.d.ts.map +1 -0
  73. package/dist/tools/read/gas.js +368 -0
  74. package/dist/tools/read/gas.js.map +1 -0
  75. package/dist/tools/{logs.d.ts → read/logs.d.ts} +6 -5
  76. package/dist/tools/read/logs.d.ts.map +1 -0
  77. package/dist/tools/{logs.js → read/logs.js} +2 -2
  78. package/dist/tools/read/logs.js.map +1 -0
  79. package/dist/tools/{signatures.d.ts → read/signatures.d.ts} +8 -4
  80. package/dist/tools/read/signatures.d.ts.map +1 -0
  81. package/dist/tools/{signatures.js → read/signatures.js} +1 -1
  82. package/dist/tools/read/signatures.js.map +1 -0
  83. package/dist/tools/read/signing.d.ts +27 -0
  84. package/dist/tools/read/signing.d.ts.map +1 -0
  85. package/dist/tools/read/signing.js +75 -0
  86. package/dist/tools/read/signing.js.map +1 -0
  87. package/dist/tools/sign/errors.d.ts +9 -0
  88. package/dist/tools/sign/errors.d.ts.map +1 -0
  89. package/dist/tools/sign/errors.js +29 -0
  90. package/dist/tools/sign/errors.js.map +1 -0
  91. package/dist/tools/{transactions.d.ts → sign/transactions.d.ts} +48 -62
  92. package/dist/tools/sign/transactions.d.ts.map +1 -0
  93. package/dist/tools/sign/transactions.js +316 -0
  94. package/dist/tools/sign/transactions.js.map +1 -0
  95. package/dist/tools/sign/wallet.d.ts +50 -0
  96. package/dist/tools/sign/wallet.d.ts.map +1 -0
  97. package/dist/tools/sign/wallet.js +180 -0
  98. package/dist/tools/sign/wallet.js.map +1 -0
  99. package/dist/utils.d.ts +5 -2
  100. package/dist/utils.d.ts.map +1 -1
  101. package/dist/utils.js +5 -2
  102. package/dist/utils.js.map +1 -1
  103. package/dist/wallet-client.d.ts +33 -4
  104. package/dist/wallet-client.d.ts.map +1 -1
  105. package/dist/wallet-client.js +90 -24
  106. package/dist/wallet-client.js.map +1 -1
  107. package/dist/walletconnect.d.ts +100 -13
  108. package/dist/walletconnect.d.ts.map +1 -1
  109. package/dist/walletconnect.js +215 -50
  110. package/dist/walletconnect.js.map +1 -1
  111. package/package.json +10 -4
  112. package/src/anvil.ts +150 -271
  113. package/src/chains.ts +14 -2
  114. package/src/client.ts +0 -1
  115. package/src/context.ts +27 -0
  116. package/src/hosted.ts +11 -0
  117. package/src/http-server.ts +30 -5
  118. package/src/index.ts +15 -12
  119. package/src/lib.ts +24 -0
  120. package/src/mcp-server.ts +47 -0
  121. package/src/oauth.ts +98 -45
  122. package/src/preview.ts +21 -2
  123. package/src/signing-requests.ts +198 -0
  124. package/src/tools/index.ts +67 -16
  125. package/src/tools/{advanced.ts → read/advanced.ts} +13 -11
  126. package/src/tools/{balance.ts → read/balance.ts} +3 -3
  127. package/src/tools/{contract-info.ts → read/contract-info.ts} +3 -3
  128. package/src/tools/{contract.ts → read/contract.ts} +3 -3
  129. package/src/tools/{ens.ts → read/ens.ts} +3 -3
  130. package/src/tools/read/gas.ts +480 -0
  131. package/src/tools/{logs.ts → read/logs.ts} +3 -3
  132. package/src/tools/{signatures.ts → read/signatures.ts} +2 -2
  133. package/src/tools/read/signing.ts +86 -0
  134. package/src/tools/sign/errors.ts +28 -0
  135. package/src/tools/sign/transactions.ts +428 -0
  136. package/src/tools/sign/wallet.ts +216 -0
  137. package/src/utils.ts +5 -2
  138. package/src/wallet-client.ts +91 -22
  139. package/src/walletconnect.ts +250 -48
  140. package/dist/tools/advanced.d.ts.map +0 -1
  141. package/dist/tools/advanced.js.map +0 -1
  142. package/dist/tools/balance.d.ts.map +0 -1
  143. package/dist/tools/balance.js.map +0 -1
  144. package/dist/tools/contract-info.d.ts.map +0 -1
  145. package/dist/tools/contract-info.js.map +0 -1
  146. package/dist/tools/contract.d.ts.map +0 -1
  147. package/dist/tools/contract.js.map +0 -1
  148. package/dist/tools/ens.d.ts.map +0 -1
  149. package/dist/tools/ens.js.map +0 -1
  150. package/dist/tools/gas.d.ts +0 -99
  151. package/dist/tools/gas.d.ts.map +0 -1
  152. package/dist/tools/gas.js +0 -181
  153. package/dist/tools/gas.js.map +0 -1
  154. package/dist/tools/logs.d.ts.map +0 -1
  155. package/dist/tools/logs.js.map +0 -1
  156. package/dist/tools/signatures.d.ts.map +0 -1
  157. package/dist/tools/signatures.js.map +0 -1
  158. package/dist/tools/transactions.d.ts.map +0 -1
  159. package/dist/tools/transactions.js +0 -365
  160. package/dist/tools/transactions.js.map +0 -1
  161. package/src/tools/gas.ts +0 -219
  162. package/src/tools/transactions.ts +0 -456
@@ -0,0 +1,480 @@
1
+ import {
2
+ type AbiFunction,
3
+ type Address,
4
+ decodeFunctionResult,
5
+ encodeFunctionData,
6
+ formatEther,
7
+ type Hex,
8
+ isAddress,
9
+ parseAbiItem,
10
+ toHex
11
+ } from 'viem'
12
+ import { simulateBlocks } from 'viem/actions'
13
+ import { z } from 'zod'
14
+ import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
15
+ import { enrichTransfers } from '../../preview.js'
16
+ import type { ChainName } from '../../types.js'
17
+ import { convertArgumentsToTypes, createTool, formatResponse } from '../../utils.js'
18
+
19
+ /** Alchemy's bundle method takes at most this many transactions. */
20
+ 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
+ /** Where eth_simulateV1's traced native transfers say they come from. */
24
+ const NATIVE_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
25
+
26
+ const argsSchema = z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
27
+
28
+ interface BundleCall {
29
+ from?: Address
30
+ to: Address
31
+ data?: Hex
32
+ value?: bigint
33
+ abi?: AbiFunction
34
+ }
35
+
36
+ interface AssetChange {
37
+ assetType: string
38
+ token?: string
39
+ symbol?: string
40
+ tokenId?: string
41
+ from: string
42
+ to: string
43
+ amount?: string
44
+ rawAmount: string
45
+ }
46
+
47
+ interface CallOutcome {
48
+ status: 'success' | 'failure'
49
+ gasUsed?: string
50
+ error?: string
51
+ result?: unknown
52
+ assetChanges: AssetChange[]
53
+ }
54
+
55
+ interface AlchemyOutcome {
56
+ changes: {
57
+ assetType: string
58
+ contractAddress?: string
59
+ symbol?: string
60
+ tokenId?: string
61
+ from: string
62
+ to: string
63
+ amount?: string
64
+ rawAmount: string
65
+ }[]
66
+ gasUsed?: Hex
67
+ error?: { message: string }
68
+ }
69
+
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
+ function bundleCall(
80
+ call: { to: string; functionAbi?: string; args?: z.infer<typeof argsSchema>; data?: string; value?: string; from?: string },
81
+ from: string | undefined,
82
+ index: number
83
+ ): BundleCall {
84
+ const sender = call.from ?? from
85
+ if (!isAddress(call.to)) throw new Error(`Call ${index}: invalid to address ${call.to}`)
86
+ if (sender && !isAddress(sender)) throw new Error(`Call ${index}: invalid from address ${sender}`)
87
+ if (call.functionAbi && call.data) throw new Error(`Call ${index}: give functionAbi and args, or data — not both`)
88
+
89
+ const abi = call.functionAbi ? (parseAbiItem(call.functionAbi) as AbiFunction) : undefined
90
+ const data = abi
91
+ ? encodeFunctionData({ abi: [abi], functionName: abi.name, args: convertArgumentsToTypes(call.args ?? [], abi.inputs) })
92
+ : (call.data as Hex | undefined)
93
+
94
+ return { from: sender as Address | undefined, to: call.to, data, value: call.value ? BigInt(call.value) : undefined, abi }
95
+ }
96
+
97
+ /** Native, ERC20, ERC721 and ERC1155 changes with token metadata — on Alchemy's paid plans only. */
98
+ async function viaAlchemy(chain: ChainName, calls: BundleCall[], blockNumber?: bigint): Promise<CallOutcome[]> {
99
+ const client = getClientManager().getClient(chain)
100
+ const transactions = calls.map((call) => ({
101
+ from: call.from,
102
+ to: call.to,
103
+ data: call.data,
104
+ value: call.value !== undefined ? toHex(call.value) : undefined
105
+ }))
106
+ const params = blockNumber === undefined ? [transactions] : [transactions, toHex(blockNumber)]
107
+
108
+ const outcomes = (await client.request({ method: 'alchemy_simulateAssetChangesBundle', params } as never)) as AlchemyOutcome[]
109
+ return outcomes.map((outcome) => ({
110
+ status: outcome.error ? 'failure' : 'success',
111
+ gasUsed: outcome.gasUsed ? BigInt(outcome.gasUsed).toString() : undefined,
112
+ error: outcome.error?.message,
113
+ assetChanges: outcome.changes.map((change) => ({
114
+ assetType: change.assetType,
115
+ token: change.contractAddress,
116
+ symbol: change.symbol,
117
+ tokenId: change.tokenId,
118
+ from: change.from,
119
+ to: change.to,
120
+ amount: change.amount,
121
+ rawAmount: change.rawAmount
122
+ }))
123
+ }))
124
+ }
125
+
126
+ function decodeResult(abi: AbiFunction | undefined, data: Hex): unknown {
127
+ if (!abi?.outputs.length) return data
128
+ try {
129
+ return decodeFunctionResult({ abi: [abi], functionName: abi.name, data })
130
+ } catch {
131
+ return data
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Any RPC with eth_simulateV1: decoded return values, and native, ERC20 and ERC721 transfers.
137
+ *
138
+ * ponytail: ERC1155 moves tokens with TransferSingle / TransferBatch, which are not read
139
+ * here, so those changes are missing on this path. Parse them if 1155 bundles matter.
140
+ */
141
+ async function viaSimulateV1(chain: ChainName, calls: BundleCall[], blockNumber?: bigint): Promise<CallOutcome[]> {
142
+ const client = getClientManager().getClient(chain)
143
+ const [block] = await simulateBlocks(client, {
144
+ blocks: [{ calls: calls.map((call) => ({ account: call.from, to: call.to, data: call.data, value: call.value })) }],
145
+ traceTransfers: true,
146
+ validation: false,
147
+ ...(blockNumber !== undefined && { blockNumber })
148
+ })
149
+ if (!block) throw new Error('The RPC returned no simulated block')
150
+
151
+ const nativeSymbol = client.chain?.nativeCurrency.symbol
152
+ return Promise.all(
153
+ block.calls.map(async (call, index): Promise<CallOutcome> => {
154
+ const error = call.error as { shortMessage?: string; message?: string } | undefined
155
+ const transfers = await enrichTransfers(chain, (call.logs ?? []) as never)
156
+
157
+ return {
158
+ status: call.status,
159
+ gasUsed: call.gasUsed.toString(),
160
+ error: call.status === 'success' ? undefined : (error?.shortMessage ?? error?.message ?? 'execution reverted'),
161
+ result: call.status === 'success' ? decodeResult(calls[index]?.abi, call.data) : undefined,
162
+ assetChanges: transfers.map((transfer) => {
163
+ if (transfer.tokenId !== undefined) {
164
+ return {
165
+ assetType: 'ERC721',
166
+ token: transfer.token,
167
+ symbol: transfer.symbol,
168
+ tokenId: transfer.tokenId,
169
+ from: transfer.from,
170
+ to: transfer.to,
171
+ amount: '1',
172
+ rawAmount: '1'
173
+ }
174
+ }
175
+
176
+ const native = transfer.token.toLowerCase() === NATIVE_TOKEN
177
+ return {
178
+ assetType: native ? 'NATIVE' : 'ERC20',
179
+ token: native ? undefined : transfer.token,
180
+ symbol: native ? nativeSymbol : transfer.symbol,
181
+ from: transfer.from,
182
+ to: transfer.to,
183
+ amount: native ? formatEther(BigInt(transfer.amount)) : transfer.humanAmount,
184
+ rawAmount: transfer.amount
185
+ }
186
+ })
187
+ }
188
+ })
189
+ )
190
+ }
191
+
192
+ export default {
193
+ simulate_contract: createTool(
194
+ 'Simulate Contract Call',
195
+ 'Simulate a contract call (including state-changing functions) without broadcasting. Returns simulation result and estimated gas.',
196
+ z.object({
197
+ chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network to simulate on'),
198
+ contractAddress: z.string().describe('Contract address to call'),
199
+ functionAbi: z
200
+ .string()
201
+ .describe('Function ABI signature (e.g., "function transfer(address to, uint256 amount)"). Can be any function type.'),
202
+ args: z
203
+ .array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
204
+ .optional()
205
+ .describe('Function arguments in order matching the ABI signature'),
206
+ from: z.string().optional().describe('Sender address (defaults to zero address)'),
207
+ value: z.string().optional().describe('ETH value to send with the transaction (in wei as string)'),
208
+ blockNumber: z.string().optional().describe('Block number for simulation (defaults to latest)')
209
+ }),
210
+ async (args) => {
211
+ if (!isAddress(args.contractAddress)) {
212
+ throw new Error(`Invalid contract address: ${args.contractAddress}`)
213
+ }
214
+
215
+ if (args.from && !isAddress(args.from)) {
216
+ throw new Error(`Invalid from address: ${args.from}`)
217
+ }
218
+
219
+ const clientManager = getClientManager()
220
+ const client = clientManager.getClient(args.chain as ChainName)
221
+
222
+ try {
223
+ const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
224
+ const convertedArgs = convertArgumentsToTypes(args.args || [], abiItem.inputs)
225
+
226
+ const blockTag = args.blockNumber ? BigInt(args.blockNumber) : undefined
227
+
228
+ // Simulate the call
229
+ const result = await client.call({
230
+ to: args.contractAddress as Address,
231
+ data: encodeFunctionData({
232
+ abi: [abiItem],
233
+ functionName: abiItem.name,
234
+ args: convertedArgs
235
+ }),
236
+ account: args.from ? (args.from as Address) : undefined,
237
+ value: args.value ? BigInt(args.value) : undefined,
238
+ blockNumber: blockTag
239
+ })
240
+
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
+ })
253
+
254
+ // Decode the result if the function has outputs
255
+ let decodedResult: unknown = result.data
256
+ if (abiItem.outputs && abiItem.outputs.length > 0 && result.data) {
257
+ decodedResult = decodeFunctionResult({
258
+ abi: [abiItem],
259
+ functionName: abiItem.name,
260
+ data: result.data
261
+ })
262
+ }
263
+
264
+ return formatResponse({
265
+ success: true,
266
+ chain: args.chain,
267
+ contractAddress: args.contractAddress,
268
+ functionName: abiItem.name,
269
+ result: decodedResult,
270
+ rawData: result.data,
271
+ gasEstimate: gasEstimate.toString(),
272
+ blockNumber: args.blockNumber || 'latest'
273
+ })
274
+ } catch (error) {
275
+ // Check if it's a revert error
276
+ const errorMessage = error instanceof Error ? error.message : String(error)
277
+
278
+ return formatResponse({
279
+ success: false,
280
+ chain: args.chain,
281
+ contractAddress: args.contractAddress,
282
+ error: errorMessage,
283
+ reverted: errorMessage.includes('revert') || errorMessage.includes('execution reverted')
284
+ })
285
+ }
286
+ }
287
+ ),
288
+
289
+ simulate_bundle: createTool(
290
+ 'Simulate Transaction Bundle',
291
+ 'Simulate several transactions in order, in one block, each seeing the state the previous ones left — e.g. ' +
292
+ 'approve then swap, or deposit then borrow. Nothing is broadcast. Returns per transaction whether it ' +
293
+ 'succeeded, gas used, the revert reason, and the assets that moved. Up to 3 transactions go through ' +
294
+ "Alchemy's asset-change simulation (native, ERC20, ERC721, ERC1155, with symbols) where the plan allows it; " +
295
+ 'otherwise eth_simulateV1, which adds decoded return values but reports only native and ERC20 transfers.',
296
+ z.object({
297
+ chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network to simulate on'),
298
+ from: z.string().optional().describe('Sender for every transaction that does not name its own'),
299
+ calls: z
300
+ .array(
301
+ z.object({
302
+ to: z.string().describe('Contract or recipient address'),
303
+ functionAbi: z
304
+ .string()
305
+ .optional()
306
+ .describe('Function signature to encode the call, e.g. "function approve(address spender, uint256 amount)"'),
307
+ args: argsSchema.optional().describe('Arguments for functionAbi, in order'),
308
+ data: z.string().optional().describe('Raw calldata, instead of functionAbi and args'),
309
+ value: z.string().optional().describe('Native value to send, in wei'),
310
+ from: z.string().optional().describe('Sender of this transaction, if not the bundle-wide from')
311
+ })
312
+ )
313
+ .min(1)
314
+ .max(MAX_BUNDLE)
315
+ .describe('Transactions to run, in order'),
316
+ blockNumber: z.string().optional().describe('Block to simulate on top of (defaults to latest)')
317
+ }),
318
+ async (args) => {
319
+ const chain = args.chain as ChainName
320
+ const calls = args.calls.map((call, index) => bundleCall(call, args.from, index))
321
+ const blockNumber = args.blockNumber ? BigInt(args.blockNumber) : undefined
322
+
323
+ let outcomes: CallOutcome[] | undefined
324
+ let via = 'alchemy_simulateAssetChangesBundle'
325
+ let fallbackReason: string | undefined
326
+
327
+ if (calls.length <= ALCHEMY_BUNDLE_LIMIT) {
328
+ try {
329
+ outcomes = await viaAlchemy(chain, calls, blockNumber)
330
+ } catch (error) {
331
+ // A free plan, a chain Alchemy does not simulate on, or a custom RPC that is not Alchemy.
332
+ fallbackReason = rpcReason(error)
333
+ }
334
+ } else {
335
+ fallbackReason = `Alchemy simulates at most ${ALCHEMY_BUNDLE_LIMIT} transactions per bundle`
336
+ }
337
+
338
+ if (!outcomes) {
339
+ via = 'eth_simulateV1'
340
+ try {
341
+ outcomes = await viaSimulateV1(chain, calls, blockNumber)
342
+ } catch (error) {
343
+ return formatResponse({ success: false, chain, error: rpcReason(error), alchemyBundleError: fallbackReason })
344
+ }
345
+ }
346
+
347
+ return formatResponse({
348
+ success: outcomes.every((outcome) => outcome.status === 'success'),
349
+ chain,
350
+ via,
351
+ ...(fallbackReason && { fallbackReason }),
352
+ totalGasUsed: outcomes.reduce((sum, outcome) => sum + BigInt(outcome.gasUsed ?? 0), 0n),
353
+ calls: outcomes.map((outcome, index) => ({
354
+ index,
355
+ to: calls[index]?.to,
356
+ ...(calls[index]?.abi && { functionName: calls[index].abi.name }),
357
+ ...outcome
358
+ }))
359
+ })
360
+ },
361
+ // Offered only where a provider RPC backs it: public endpoints rarely serve eth_simulateV1.
362
+ () => Boolean(getClientManager().getConfig().alchemyApiKey)
363
+ ),
364
+
365
+ estimate_gas: createTool(
366
+ 'Estimate Gas',
367
+ 'Estimate gas required for a transaction. Supports contract calls, transfers, and deployments.',
368
+ z.object({
369
+ chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
370
+ to: z.string().optional().describe('Recipient address (omit for contract deployment)'),
371
+ from: z.string().optional().describe('Sender address (optional)'),
372
+ value: z.string().optional().describe('ETH value to send (in wei as string)'),
373
+ data: z.string().optional().describe('Transaction data (hex string for contract calls or deployment bytecode)'),
374
+ functionAbi: z.string().optional().describe('Optional: Function ABI signature to encode call data automatically'),
375
+ args: z
376
+ .array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
377
+ .optional()
378
+ .describe('Optional: Function arguments (only used with functionAbi)')
379
+ }),
380
+ async (args) => {
381
+ if (args.to && !isAddress(args.to)) {
382
+ throw new Error(`Invalid to address: ${args.to}`)
383
+ }
384
+
385
+ if (args.from && !isAddress(args.from)) {
386
+ throw new Error(`Invalid from address: ${args.from}`)
387
+ }
388
+
389
+ const clientManager = getClientManager()
390
+ const client = clientManager.getClient(args.chain as ChainName)
391
+
392
+ let callData = args.data
393
+
394
+ // If functionAbi is provided, encode the call data
395
+ if (args.functionAbi) {
396
+ const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
397
+ const convertedArgs = convertArgumentsToTypes(args.args || [], abiItem.inputs)
398
+ callData = encodeFunctionData({
399
+ abi: [abiItem],
400
+ functionName: abiItem.name,
401
+ args: convertedArgs
402
+ })
403
+ }
404
+
405
+ const gasEstimate = await client.estimateGas({
406
+ to: args.to ? (args.to as Address) : undefined,
407
+ account: args.from ? (args.from as Address) : undefined,
408
+ value: args.value ? BigInt(args.value) : undefined,
409
+ data: callData as `0x${string}` | undefined
410
+ })
411
+
412
+ return formatResponse({
413
+ success: true,
414
+ chain: args.chain,
415
+ gasEstimate: gasEstimate.toString(),
416
+ to: args.to,
417
+ from: args.from,
418
+ value: args.value
419
+ })
420
+ }
421
+ ),
422
+
423
+ get_gas_price: createTool(
424
+ 'Get Gas Price',
425
+ 'Get current gas prices for a chain. Returns both legacy gasPrice and EIP-1559 fees (maxFeePerGas, maxPriorityFeePerGas).',
426
+ z.object({
427
+ chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
428
+ formatted: z.boolean().optional().default(true).describe('Return prices in Gwei (default: true). If false, returns wei.')
429
+ }),
430
+ async (args) => {
431
+ const clientManager = getClientManager()
432
+ const client = clientManager.getClient(args.chain as ChainName)
433
+
434
+ // Get both legacy and EIP-1559 gas prices
435
+ const [gasPrice, feeData] = await Promise.all([
436
+ client.getGasPrice(),
437
+ client.estimateFeesPerGas().catch(() => null) // Some chains don't support EIP-1559
438
+ ])
439
+
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
+ }
448
+
449
+ const response: any = {
450
+ chain: args.chain,
451
+ timestamp: new Date().toISOString(),
452
+ legacy: {
453
+ gasPrice: args.formatted ? formatPrice(gasPrice) : gasPrice.toString(),
454
+ gasPriceWei: gasPrice.toString()
455
+ }
456
+ }
457
+
458
+ // Add EIP-1559 data if available
459
+ if (feeData) {
460
+ response.eip1559 = {
461
+ maxFeePerGas: args.formatted ? formatPrice(feeData.maxFeePerGas) : feeData.maxFeePerGas.toString(),
462
+ maxPriorityFeePerGas: args.formatted
463
+ ? formatPrice(feeData.maxPriorityFeePerGas)
464
+ : feeData.maxPriorityFeePerGas.toString(),
465
+ maxFeePerGasWei: feeData.maxFeePerGas.toString(),
466
+ maxPriorityFeePerGasWei: feeData.maxPriorityFeePerGas.toString()
467
+ }
468
+
469
+ // Calculate estimated total cost for a standard 21000 gas transaction
470
+ const standardGasLimit = 21000n
471
+ const estimatedCost = feeData.maxFeePerGas * standardGasLimit
472
+ response.eip1559.estimatedCostFor21kGas = args.formatted
473
+ ? `${(Number(estimatedCost) / 1e18).toFixed(6)} ETH`
474
+ : estimatedCost.toString()
475
+ }
476
+
477
+ return formatResponse(response)
478
+ }
479
+ )
480
+ }
@@ -13,9 +13,9 @@ import {
13
13
  toEventSignature
14
14
  } from 'viem'
15
15
  import { z } from 'zod'
16
- import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
17
- import type { ChainName } from '../types.js'
18
- import { convertEventArgsToTypes, createTool, formatResponse } from '../utils.js'
16
+ import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
17
+ import type { ChainName } from '../../types.js'
18
+ import { convertEventArgsToTypes, createTool, formatResponse } from '../../utils.js'
19
19
 
20
20
  /**
21
21
  * A topic is always 32 bytes, so an address, a number or a short hex value has to be
@@ -10,8 +10,8 @@ import {
10
10
  toFunctionSignature
11
11
  } from 'viem'
12
12
  import { z } from 'zod'
13
- import type { AbiError } from '../types.js'
14
- import { createTool, formatResponse } from '../utils.js'
13
+ import type { AbiError } from '../../types.js'
14
+ import { createTool, formatResponse } from '../../utils.js'
15
15
 
16
16
  const FunctionAbiSchema = z.object({
17
17
  functionAbi: z.string().describe('Function ABI definition (e.g., "function transfer(address to, uint256 amount)")')
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod'
2
+ import { collectSigningRequest } from '../../signing-requests.js'
3
+ import { createTool, formatResponse } from '../../utils.js'
4
+ import { failure } from '../sign/errors.js'
5
+
6
+ /**
7
+ * Collecting a signature that was not given within the tool call that asked for it.
8
+ *
9
+ * A read rather than a signing tool on purpose: the signature was requested — and charged
10
+ * for — once already, and a host metering signing should not bill an agent again for asking
11
+ * whether the human has got round to it yet.
12
+ */
13
+ export default {
14
+ check_signing_request: createTool(
15
+ 'Check Signing Request',
16
+ 'Collect the result of a signing request that was still waiting for approval. Waits a while for the user to approve, then reports back. Call this instead of asking for the signature again — repeating the original tool would put a second request in front of the wallet.',
17
+ z.object({
18
+ requestId: z.string().describe('The requestId returned when the signing tool said it was awaiting approval')
19
+ }),
20
+ async (args, identity) => {
21
+ try {
22
+ const request = await collectSigningRequest(args.requestId, identity)
23
+
24
+ const waitedFor = `${Math.round((Date.now() - request.startedAt) / 1000)}s`
25
+
26
+ if (request.state === 'pending') {
27
+ // Approved already and waiting on the chain is a different thing to report: there
28
+ // is nothing left for the user to do, and nothing to chase them about.
29
+ if (request.stage === 'mining') {
30
+ return formatResponse({
31
+ success: true,
32
+ status: 'mining',
33
+ requestId: request.id,
34
+ what: request.summary,
35
+ transactionHash: request.txHash,
36
+ waitingFor: waitedFor,
37
+ nextStep: 'Approved and broadcast. Call this again with the same requestId to get the mined result.',
38
+ message: 'Signed and sent. Waiting for it to be mined.'
39
+ })
40
+ }
41
+
42
+ return formatResponse({
43
+ success: true,
44
+ status: 'awaiting_approval',
45
+ requestId: request.id,
46
+ what: request.summary,
47
+ waitingFor: waitedFor,
48
+ nextStep:
49
+ 'Still not approved. Ask the user to check their wallet and approve or reject it, then call this again with the same requestId.',
50
+ message: 'Nothing has been signed yet.'
51
+ })
52
+ }
53
+
54
+ if (request.state === 'failed') {
55
+ return formatResponse({
56
+ success: false,
57
+ status: 'rejected_or_failed',
58
+ what: request.summary,
59
+ error: request.error,
60
+ message: 'Nothing was signed. The wallet rejected it, or it expired before anybody approved.'
61
+ })
62
+ }
63
+
64
+ // A transaction carries how it ended on the chain; a message signature is just the
65
+ // signature, with nothing to mine.
66
+ const settled = request.result as { receipt?: { status: string; blockNumber?: string; gasUsed?: string } } | undefined
67
+ const reverted = settled?.receipt?.status === 'reverted'
68
+
69
+ return formatResponse({
70
+ success: !reverted,
71
+ status: settled?.receipt?.status ?? 'signed',
72
+ what: request.summary,
73
+ signedWith: request.signer,
74
+ result: request.result,
75
+ message: reverted
76
+ ? 'Mined, but it reverted. The gas was spent and nothing else changed.'
77
+ : settled?.receipt?.status === 'broadcast'
78
+ ? 'Approved and broadcast, but not mined yet.'
79
+ : 'Approved and done.'
80
+ })
81
+ } catch (error) {
82
+ return failure(error, 'Could not check that signing request')
83
+ }
84
+ }
85
+ )
86
+ }
@@ -0,0 +1,28 @@
1
+ import { log } from '../../log.js'
2
+ import { formatResponse } from '../../utils.js'
3
+
4
+ /**
5
+ * WalletConnect rejects with a plain `{ code, message }` rather than an Error, which
6
+ * String() renders as "[object Object]" — the reason a wallet gave for refusing is the
7
+ * whole point of the message, so dig it out.
8
+ */
9
+ export function describeError(error: unknown): string {
10
+ if (error instanceof Error) return error.message
11
+ if (error && typeof error === 'object') {
12
+ const { message, code, reason } = error as { message?: string; code?: number; reason?: string }
13
+ const text = message ?? reason
14
+ if (text) return code === undefined ? text : `${text} (code ${code})`
15
+ return JSON.stringify(error)
16
+ }
17
+ return String(error)
18
+ }
19
+
20
+ /** A refused or failed signing attempt, reported to the caller and to the log alike. */
21
+ export function failure(error: unknown, message: string) {
22
+ log('warning', 'Transaction', `${message} — ${describeError(error)}`)
23
+ return formatResponse({
24
+ success: false,
25
+ error: describeError(error),
26
+ message
27
+ })
28
+ }