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
|
@@ -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
|
+
}
|