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