web3-tools-mcp 3.0.0 → 3.2.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 +115 -2
- package/README.md +96 -60
- 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 +609 -65
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +89 -28
- 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 +2 -4
- package/dist/client.js.map +1 -1
- package/dist/http-server.d.ts +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +12 -5
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/kv-storage.d.ts +2 -1
- package/dist/kv-storage.d.ts.map +1 -1
- package/dist/kv-storage.js +5 -2
- package/dist/kv-storage.js.map +1 -1
- package/dist/oauth.d.ts +18 -4
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +62 -26
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +7 -5
- package/dist/preview.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +16 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read/advanced.d.ts +25 -20
- package/dist/tools/read/advanced.d.ts.map +1 -1
- package/dist/tools/read/advanced.js +45 -33
- package/dist/tools/read/advanced.js.map +1 -1
- package/dist/tools/read/balance.d.ts +6 -6
- package/dist/tools/read/balance.d.ts.map +1 -1
- package/dist/tools/read/balance.js +17 -32
- package/dist/tools/read/balance.js.map +1 -1
- package/dist/tools/read/contract-info.d.ts +14 -12
- package/dist/tools/read/contract-info.d.ts.map +1 -1
- package/dist/tools/read/contract-info.js +53 -122
- package/dist/tools/read/contract-info.js.map +1 -1
- package/dist/tools/read/contract.d.ts +10 -10
- package/dist/tools/read/contract.d.ts.map +1 -1
- package/dist/tools/read/contract.js +2 -1
- package/dist/tools/read/contract.js.map +1 -1
- package/dist/tools/read/defi.d.ts +86 -0
- package/dist/tools/read/defi.d.ts.map +1 -0
- package/dist/tools/read/defi.js +210 -0
- package/dist/tools/read/defi.js.map +1 -0
- package/dist/tools/read/ens.d.ts +20 -40
- package/dist/tools/read/ens.d.ts.map +1 -1
- package/dist/tools/read/ens.js +25 -33
- package/dist/tools/read/ens.js.map +1 -1
- package/dist/tools/read/gas.d.ts +16 -16
- package/dist/tools/read/gas.d.ts.map +1 -1
- package/dist/tools/read/gas.js +13 -48
- package/dist/tools/read/gas.js.map +1 -1
- package/dist/tools/read/logs.d.ts +65 -4
- package/dist/tools/read/logs.d.ts.map +1 -1
- package/dist/tools/read/logs.js +96 -115
- package/dist/tools/read/logs.js.map +1 -1
- package/dist/tools/read/signatures.d.ts.map +1 -1
- package/dist/tools/read/signatures.js +4 -3
- package/dist/tools/read/signatures.js.map +1 -1
- package/dist/tools/read/signing.d.ts.map +1 -1
- package/dist/tools/read/signing.js +8 -2
- package/dist/tools/read/signing.js.map +1 -1
- package/dist/tools/sign/transactions.d.ts +16 -16
- package/dist/tools/sign/transactions.d.ts.map +1 -1
- package/dist/tools/sign/transactions.js +54 -9
- package/dist/tools/sign/transactions.js.map +1 -1
- package/dist/tools/sign/wallet.d.ts +4 -4
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +43 -2
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +1 -0
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +18 -5
- package/dist/wallet-client.js.map +1 -1
- package/package.json +4 -7
- package/src/chain-meta.ts +16 -7
- package/src/chains.ts +101 -31
- package/src/clear-signing.ts +15 -9
- package/src/client.ts +2 -3
- package/src/http-server.ts +11 -6
- package/src/index.ts +5 -6
- package/src/kv-storage.ts +6 -3
- package/src/oauth.ts +93 -34
- package/src/preview.ts +7 -5
- package/src/tools/index.ts +14 -1
- package/src/tools/read/advanced.ts +63 -34
- package/src/tools/read/balance.ts +18 -33
- package/src/tools/read/contract-info.ts +64 -144
- package/src/tools/read/contract.ts +2 -1
- package/src/tools/read/defi.ts +287 -0
- package/src/tools/read/ens.ts +26 -33
- package/src/tools/read/gas.ts +13 -50
- package/src/tools/read/logs.ts +131 -143
- package/src/tools/read/signatures.ts +4 -4
- package/src/tools/read/signing.ts +8 -2
- package/src/tools/sign/transactions.ts +60 -9
- package/src/types.ts +0 -1
- package/src/utils.ts +47 -2
- package/src/wallet-client.ts +26 -5
package/src/tools/read/ens.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Address, isAddress, namehash } from 'viem'
|
|
2
2
|
import { normalize } from 'viem/ens'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
-
import { getClientManager
|
|
4
|
+
import { getClientManager } from '../../client.js'
|
|
5
5
|
import type { ChainName } from '../../types.js'
|
|
6
|
-
import { createTool, formatResponse } from '../../utils.js'
|
|
6
|
+
import { createTool, formatResponse, MAX_BATCH } from '../../utils.js'
|
|
7
7
|
|
|
8
8
|
// Base ENS Contract Addresses
|
|
9
9
|
const BASE_ENS_CONTRACTS = {
|
|
@@ -159,32 +159,32 @@ async function getTextRecordBase(name: string, key: string): Promise<string | nu
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** Where a name service is deployed: ENS on mainnet, Linea Names on Linea, Basenames on Base. */
|
|
163
|
+
const ENS_CHAINS = ['mainnet', 'linea', 'base'] as const
|
|
164
|
+
const ensChain = z
|
|
165
|
+
.enum(ENS_CHAINS)
|
|
166
|
+
.default('mainnet')
|
|
167
|
+
.describe('Where the name lives: mainnet for .eth (ENS), linea for Linea Names, base for Basenames. Defaults to mainnet.')
|
|
168
|
+
|
|
162
169
|
const EnsResolveSchema = z.object({
|
|
163
|
-
chain:
|
|
164
|
-
|
|
165
|
-
.default('mainnet')
|
|
166
|
-
.describe('Blockchain network (defaults to mainnet, as ENS is primarily on Ethereum)'),
|
|
167
|
-
name: z.string().describe("ENS name to resolve to an address (e.g., 'vitalik.eth', 'example.eth')"),
|
|
168
|
-
universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
|
|
170
|
+
chain: ensChain,
|
|
171
|
+
name: z.string().describe("ENS name to resolve to an address (e.g., 'vitalik.eth', 'example.eth')")
|
|
169
172
|
})
|
|
170
173
|
|
|
171
174
|
const EnsReverseResolveSchema = z.object({
|
|
172
|
-
chain:
|
|
173
|
-
address: z.string().describe('Ethereum address to resolve to an ENS name')
|
|
174
|
-
universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
|
|
175
|
+
chain: ensChain,
|
|
176
|
+
address: z.string().describe('Ethereum address to resolve to an ENS name')
|
|
175
177
|
})
|
|
176
178
|
|
|
177
179
|
const EnsTextRecordSchema = z.object({
|
|
178
|
-
chain:
|
|
180
|
+
chain: ensChain,
|
|
179
181
|
name: z.string().describe('ENS name to query text records from'),
|
|
180
|
-
key: z.string().describe("Text record key (e.g., 'avatar', 'description', 'email', 'url', 'com.twitter', 'com.github')")
|
|
181
|
-
universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
|
|
182
|
+
key: z.string().describe("Text record key (e.g., 'avatar', 'description', 'email', 'url', 'com.twitter', 'com.github')")
|
|
182
183
|
})
|
|
183
184
|
|
|
184
185
|
const EnsBatchResolveSchema = z.object({
|
|
185
|
-
chain:
|
|
186
|
-
names: z.array(z.string()).describe('Array of ENS names to resolve to addresses in a single batch')
|
|
187
|
-
universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
|
|
186
|
+
chain: ensChain,
|
|
187
|
+
names: z.array(z.string()).max(MAX_BATCH).describe('Array of ENS names to resolve to addresses in a single batch')
|
|
188
188
|
})
|
|
189
189
|
|
|
190
190
|
export default {
|
|
@@ -207,8 +207,7 @@ export default {
|
|
|
207
207
|
const client = clientManager.getClient(args.chain as ChainName)
|
|
208
208
|
|
|
209
209
|
address = await client.getEnsAddress({
|
|
210
|
-
name: normalizedName
|
|
211
|
-
universalResolverAddress: args.universalResolver !== false ? undefined : undefined
|
|
210
|
+
name: normalizedName
|
|
212
211
|
})
|
|
213
212
|
}
|
|
214
213
|
|
|
@@ -253,8 +252,7 @@ export default {
|
|
|
253
252
|
const client = clientManager.getClient(args.chain as ChainName)
|
|
254
253
|
|
|
255
254
|
ensName = await client.getEnsName({
|
|
256
|
-
address: args.address as Address
|
|
257
|
-
universalResolverAddress: args.universalResolver !== false ? undefined : undefined
|
|
255
|
+
address: args.address as Address
|
|
258
256
|
})
|
|
259
257
|
}
|
|
260
258
|
|
|
@@ -297,8 +295,7 @@ export default {
|
|
|
297
295
|
|
|
298
296
|
textRecord = await client.getEnsText({
|
|
299
297
|
name: normalizedName,
|
|
300
|
-
key: args.key
|
|
301
|
-
universalResolverAddress: args.universalResolver !== false ? undefined : undefined
|
|
298
|
+
key: args.key
|
|
302
299
|
})
|
|
303
300
|
}
|
|
304
301
|
|
|
@@ -327,11 +324,10 @@ export default {
|
|
|
327
324
|
|
|
328
325
|
get_ens_avatar: createTool(
|
|
329
326
|
'Get ENS Avatar',
|
|
330
|
-
'Get the avatar
|
|
327
|
+
'Get the raw avatar text record for an ENS name: an https/ipfs URL, a data URI, or an NFT reference (eip155:1/erc721:0x…/1). It is not fetched or resolved to an image.',
|
|
331
328
|
z.object({
|
|
332
|
-
chain:
|
|
333
|
-
name: z.string().describe('ENS name to get avatar from')
|
|
334
|
-
universalResolver: z.boolean().optional().describe('Use universal resolver for cross-chain resolution (defaults to true)')
|
|
329
|
+
chain: ensChain,
|
|
330
|
+
name: z.string().describe('ENS name to get avatar from')
|
|
335
331
|
}),
|
|
336
332
|
async (args) => {
|
|
337
333
|
// Normalize the ENS name
|
|
@@ -347,10 +343,8 @@ export default {
|
|
|
347
343
|
const clientManager = getClientManager()
|
|
348
344
|
const client = clientManager.getClient(args.chain as ChainName)
|
|
349
345
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
universalResolverAddress: args.universalResolver !== false ? undefined : undefined
|
|
353
|
-
})
|
|
346
|
+
// getEnsAvatar would fetch the record's URL (or the NFT's metadata) from this server.
|
|
347
|
+
avatar = await client.getEnsText({ name: normalizedName, key: 'avatar' })
|
|
354
348
|
}
|
|
355
349
|
|
|
356
350
|
if (!avatar) {
|
|
@@ -394,8 +388,7 @@ export default {
|
|
|
394
388
|
const client = clientManager.getClient(args.chain as ChainName)
|
|
395
389
|
|
|
396
390
|
address = await client.getEnsAddress({
|
|
397
|
-
name: normalizedName
|
|
398
|
-
universalResolverAddress: args.universalResolver !== false ? undefined : undefined
|
|
391
|
+
name: normalizedName
|
|
399
392
|
})
|
|
400
393
|
}
|
|
401
394
|
|
package/src/tools/read/gas.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
decodeFunctionResult,
|
|
5
5
|
encodeFunctionData,
|
|
6
6
|
formatEther,
|
|
7
|
+
formatGwei,
|
|
7
8
|
type Hex,
|
|
8
9
|
isAddress,
|
|
9
10
|
parseAbiItem,
|
|
@@ -14,12 +15,10 @@ import { z } from 'zod'
|
|
|
14
15
|
import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
|
|
15
16
|
import { enrichTransfers } from '../../preview.js'
|
|
16
17
|
import type { ChainName } from '../../types.js'
|
|
17
|
-
import { convertArgumentsToTypes, createTool, formatResponse } from '../../utils.js'
|
|
18
|
+
import { convertArgumentsToTypes, createTool, formatResponse, MAX_BATCH, rpcReason } from '../../utils.js'
|
|
18
19
|
|
|
19
20
|
/** Alchemy's bundle method takes at most this many transactions. */
|
|
20
21
|
const ALCHEMY_BUNDLE_LIMIT = 3
|
|
21
|
-
/** One tool call is one quota unit, so an unbounded bundle would be a way around the quota. */
|
|
22
|
-
const MAX_BUNDLE = 25
|
|
23
22
|
/** Where eth_simulateV1's traced native transfers say they come from. */
|
|
24
23
|
const NATIVE_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
|
|
25
24
|
|
|
@@ -67,15 +66,6 @@ interface AlchemyOutcome {
|
|
|
67
66
|
error?: { message: string }
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
/**
|
|
71
|
-
* Why an RPC call failed, without the request URL — viem puts it in `message`, and with a
|
|
72
|
-
* provider key configured that URL carries the key.
|
|
73
|
-
*/
|
|
74
|
-
function rpcReason(error: unknown): string {
|
|
75
|
-
const { details, shortMessage } = error as { details?: string; shortMessage?: string }
|
|
76
|
-
return details || shortMessage || 'The RPC request failed'
|
|
77
|
-
}
|
|
78
|
-
|
|
79
69
|
function bundleCall(
|
|
80
70
|
call: { to: string; functionAbi?: string; args?: z.infer<typeof argsSchema>; data?: string; value?: string; from?: string },
|
|
81
71
|
from: string | undefined,
|
|
@@ -223,33 +213,16 @@ export default {
|
|
|
223
213
|
const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
|
|
224
214
|
const convertedArgs = convertArgumentsToTypes(args.args || [], abiItem.inputs)
|
|
225
215
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
// Simulate the call
|
|
229
|
-
const result = await client.call({
|
|
216
|
+
const tx = {
|
|
230
217
|
to: args.contractAddress as Address,
|
|
231
|
-
data: encodeFunctionData({
|
|
232
|
-
abi: [abiItem],
|
|
233
|
-
functionName: abiItem.name,
|
|
234
|
-
args: convertedArgs
|
|
235
|
-
}),
|
|
218
|
+
data: encodeFunctionData({ abi: [abiItem], functionName: abiItem.name, args: convertedArgs }),
|
|
236
219
|
account: args.from ? (args.from as Address) : undefined,
|
|
237
220
|
value: args.value ? BigInt(args.value) : undefined,
|
|
238
|
-
blockNumber:
|
|
239
|
-
}
|
|
221
|
+
blockNumber: args.blockNumber ? BigInt(args.blockNumber) : undefined
|
|
222
|
+
}
|
|
240
223
|
|
|
241
|
-
|
|
242
|
-
const gasEstimate = await client.estimateGas(
|
|
243
|
-
to: args.contractAddress as Address,
|
|
244
|
-
data: encodeFunctionData({
|
|
245
|
-
abi: [abiItem],
|
|
246
|
-
functionName: abiItem.name,
|
|
247
|
-
args: convertedArgs
|
|
248
|
-
}),
|
|
249
|
-
account: args.from ? (args.from as Address) : undefined,
|
|
250
|
-
value: args.value ? BigInt(args.value) : undefined,
|
|
251
|
-
blockNumber: blockTag
|
|
252
|
-
})
|
|
224
|
+
const result = await client.call(tx)
|
|
225
|
+
const gasEstimate = await client.estimateGas(tx)
|
|
253
226
|
|
|
254
227
|
// Decode the result if the function has outputs
|
|
255
228
|
let decodedResult: unknown = result.data
|
|
@@ -272,15 +245,12 @@ export default {
|
|
|
272
245
|
blockNumber: args.blockNumber || 'latest'
|
|
273
246
|
})
|
|
274
247
|
} catch (error) {
|
|
275
|
-
// Check if it's a revert error
|
|
276
|
-
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
277
|
-
|
|
278
248
|
return formatResponse({
|
|
279
249
|
success: false,
|
|
280
250
|
chain: args.chain,
|
|
281
251
|
contractAddress: args.contractAddress,
|
|
282
|
-
error:
|
|
283
|
-
reverted:
|
|
252
|
+
error: rpcReason(error),
|
|
253
|
+
reverted: String(error instanceof Error ? error.message : error).includes('revert')
|
|
284
254
|
})
|
|
285
255
|
}
|
|
286
256
|
}
|
|
@@ -311,7 +281,7 @@ export default {
|
|
|
311
281
|
})
|
|
312
282
|
)
|
|
313
283
|
.min(1)
|
|
314
|
-
.max(
|
|
284
|
+
.max(MAX_BATCH)
|
|
315
285
|
.describe('Transactions to run, in order'),
|
|
316
286
|
blockNumber: z.string().optional().describe('Block to simulate on top of (defaults to latest)')
|
|
317
287
|
}),
|
|
@@ -437,14 +407,7 @@ export default {
|
|
|
437
407
|
client.estimateFeesPerGas().catch(() => null) // Some chains don't support EIP-1559
|
|
438
408
|
])
|
|
439
409
|
|
|
440
|
-
const formatPrice = (wei: bigint): string => {
|
|
441
|
-
if (args.formatted) {
|
|
442
|
-
// Convert to Gwei (1 Gwei = 1e9 wei)
|
|
443
|
-
const gwei = Number(wei) / 1e9
|
|
444
|
-
return `${gwei.toFixed(2)} Gwei`
|
|
445
|
-
}
|
|
446
|
-
return wei.toString()
|
|
447
|
-
}
|
|
410
|
+
const formatPrice = (wei: bigint): string => `${formatGwei(wei)} Gwei`
|
|
448
411
|
|
|
449
412
|
const response: any = {
|
|
450
413
|
chain: args.chain,
|
|
@@ -470,7 +433,7 @@ export default {
|
|
|
470
433
|
const standardGasLimit = 21000n
|
|
471
434
|
const estimatedCost = feeData.maxFeePerGas * standardGasLimit
|
|
472
435
|
response.eip1559.estimatedCostFor21kGas = args.formatted
|
|
473
|
-
? `${(
|
|
436
|
+
? `${formatEther(estimatedCost)} ${client.chain?.nativeCurrency.symbol ?? 'ETH'}`
|
|
474
437
|
: estimatedCost.toString()
|
|
475
438
|
}
|
|
476
439
|
|
package/src/tools/read/logs.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { z } from 'zod'
|
|
16
16
|
import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
|
|
17
17
|
import type { ChainName } from '../../types.js'
|
|
18
|
-
import { convertEventArgsToTypes, createTool, formatResponse } from '../../utils.js'
|
|
18
|
+
import { convertEventArgsToTypes, createTool, formatResponse, rpcReason } 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
|
|
@@ -34,15 +34,71 @@ function toTopic(value: unknown): Hex {
|
|
|
34
34
|
throw new Error(`Cannot filter on indexed value ${JSON.stringify(value)} — expected an address, number or hex string`)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** More than this is cut short, with the block to resume from. */
|
|
38
|
+
export const MAX_LOGS = 1000
|
|
39
|
+
|
|
40
|
+
interface DecodedLog {
|
|
41
|
+
address: Address
|
|
42
|
+
blockHash: Hex | null
|
|
43
|
+
blockNumber: bigint | null
|
|
44
|
+
data: Hex
|
|
45
|
+
logIndex: number | null | undefined
|
|
46
|
+
topics: readonly Hex[]
|
|
47
|
+
transactionHash: Hex | null
|
|
48
|
+
transactionIndex: number | null | undefined
|
|
49
|
+
eventName: string
|
|
50
|
+
args: unknown
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function serializeLog(log: DecodedLog) {
|
|
54
|
+
return {
|
|
55
|
+
address: log.address,
|
|
56
|
+
blockHash: log.blockHash,
|
|
57
|
+
blockNumber: log.blockNumber?.toString(),
|
|
58
|
+
data: log.data,
|
|
59
|
+
logIndex: log.logIndex,
|
|
60
|
+
topics: log.topics,
|
|
61
|
+
transactionHash: log.transactionHash,
|
|
62
|
+
transactionIndex: log.transactionIndex,
|
|
63
|
+
decoded: { eventName: log.eventName, args: log.args }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Resuming from `nextBlock` may repeat logs of that block already returned. */
|
|
68
|
+
export function capLogs(logs: DecodedLog[]) {
|
|
69
|
+
if (logs.length <= MAX_LOGS) return { logs: logs.map(serializeLog) }
|
|
70
|
+
return {
|
|
71
|
+
logs: logs.slice(0, MAX_LOGS).map(serializeLog),
|
|
72
|
+
truncated: true,
|
|
73
|
+
nextBlock: logs[MAX_LOGS]?.blockNumber?.toString()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The same range eth_getLogs would scan — each end defaults to the latest block — in
|
|
79
|
+
* Hypersync's terms, where `toBlock` is exclusive.
|
|
80
|
+
*/
|
|
81
|
+
export function hypersyncRange(fromBlock: string | undefined, toBlock: string | undefined, latest: number) {
|
|
82
|
+
return {
|
|
83
|
+
fromBlock: fromBlock ? Number(BigInt(fromBlock)) : latest,
|
|
84
|
+
toBlock: (toBlock ? Number(BigInt(toBlock)) : latest) + 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One topic, or a list of alternatives for an OR filter. */
|
|
89
|
+
export function toTopics(value: unknown): Hex | Hex[] {
|
|
90
|
+
return Array.isArray(value) ? value.map(toTopic) : toTopic(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
37
93
|
async function getLogsWithHypersync(
|
|
38
94
|
chainName: ChainName,
|
|
39
|
-
|
|
95
|
+
abiItem: AbiEvent,
|
|
40
96
|
hypersyncApiKey?: string,
|
|
41
97
|
address?: string,
|
|
42
98
|
fromBlock?: string,
|
|
43
99
|
toBlock?: string,
|
|
44
100
|
eventArgs?: Record<string, unknown>
|
|
45
|
-
) {
|
|
101
|
+
): Promise<DecodedLog[]> {
|
|
46
102
|
const hypersyncUrl = getClientManager().getHypersyncUrl(chainName)
|
|
47
103
|
if (!hypersyncUrl) {
|
|
48
104
|
throw new Error(`Hypersync not supported for chain: ${chainName}`)
|
|
@@ -53,25 +109,26 @@ async function getLogsWithHypersync(
|
|
|
53
109
|
bearerToken: hypersyncApiKey
|
|
54
110
|
})
|
|
55
111
|
|
|
56
|
-
const
|
|
57
|
-
const eventSignature = toEventSignature(abiItem)
|
|
58
|
-
const topic0 = keccak256(toBytes(eventSignature))
|
|
112
|
+
const topic0 = keccak256(toBytes(toEventSignature(abiItem)))
|
|
59
113
|
|
|
60
114
|
// Build topics array for filtering
|
|
61
|
-
const topics: (
|
|
115
|
+
const topics: (Hex | Hex[] | null)[] = [topic0]
|
|
62
116
|
|
|
63
117
|
// Add indexed parameter filtering if eventArgs provided
|
|
64
|
-
if (eventArgs
|
|
118
|
+
if (eventArgs) {
|
|
65
119
|
const indexedInputs = abiItem.inputs.filter((input) => input.indexed)
|
|
66
120
|
|
|
67
121
|
for (let i = 0; i < indexedInputs.length && i < 3; i++) {
|
|
68
122
|
const input = indexedInputs[i]
|
|
69
123
|
const argValue = input?.name ? eventArgs[input.name] : undefined
|
|
70
124
|
|
|
71
|
-
topics.push(argValue === undefined ? null :
|
|
125
|
+
topics.push(argValue === undefined ? null : toTopics(argValue))
|
|
72
126
|
}
|
|
73
127
|
}
|
|
74
128
|
|
|
129
|
+
const latest = fromBlock && toBlock ? 0 : await client.getHeight()
|
|
130
|
+
const range = hypersyncRange(fromBlock, toBlock, latest)
|
|
131
|
+
|
|
75
132
|
const query = {
|
|
76
133
|
fieldSelection: {
|
|
77
134
|
log: [
|
|
@@ -88,8 +145,7 @@ async function getLogsWithHypersync(
|
|
|
88
145
|
LogField.LogIndex
|
|
89
146
|
]
|
|
90
147
|
},
|
|
91
|
-
|
|
92
|
-
toBlock: toBlock ? Number.parseInt(toBlock, 10) : undefined,
|
|
148
|
+
...range,
|
|
93
149
|
logs: [
|
|
94
150
|
{
|
|
95
151
|
address: address ? [address] : undefined,
|
|
@@ -100,11 +156,17 @@ async function getLogsWithHypersync(
|
|
|
100
156
|
]
|
|
101
157
|
}
|
|
102
158
|
|
|
103
|
-
|
|
159
|
+
// One response covers only part of a long range; `nextBlock` says where it stopped.
|
|
160
|
+
const logs: Log[] = []
|
|
161
|
+
for (let from = range.fromBlock; from < range.toBlock && logs.length <= MAX_LOGS; ) {
|
|
162
|
+
const res = await client.get({ ...query, fromBlock: from })
|
|
163
|
+
logs.push(...res.data.logs)
|
|
164
|
+
if (res.nextBlock <= from) break
|
|
165
|
+
from = res.nextBlock
|
|
166
|
+
}
|
|
104
167
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.map((log: Log) => {
|
|
168
|
+
return logs
|
|
169
|
+
.map((log: Log): DecodedLog | null => {
|
|
108
170
|
const topics = log.topics.filter((topic): topic is string => Boolean(topic)) as [Hex, ...Hex[]]
|
|
109
171
|
const data = (log.data ?? '0x') as Hex
|
|
110
172
|
|
|
@@ -113,7 +175,7 @@ async function getLogsWithHypersync(
|
|
|
113
175
|
return {
|
|
114
176
|
address: log.address as Address,
|
|
115
177
|
blockHash: log.blockHash as Hex,
|
|
116
|
-
blockNumber: log.blockNumber ? BigInt(log.blockNumber) :
|
|
178
|
+
blockNumber: log.blockNumber !== undefined ? BigInt(log.blockNumber) : null,
|
|
117
179
|
data,
|
|
118
180
|
logIndex: log.logIndex,
|
|
119
181
|
topics,
|
|
@@ -127,165 +189,91 @@ async function getLogsWithHypersync(
|
|
|
127
189
|
return null
|
|
128
190
|
}
|
|
129
191
|
})
|
|
130
|
-
.filter((log): log is
|
|
131
|
-
|
|
132
|
-
return decodedLogs
|
|
192
|
+
.filter((log): log is DecodedLog => log !== null)
|
|
133
193
|
}
|
|
134
194
|
|
|
135
195
|
export default {
|
|
136
196
|
get_logs: createTool(
|
|
137
197
|
'Query Contract Events',
|
|
138
|
-
|
|
198
|
+
`Search and decode contract events with filtering. Uses viem with hypersync fallback. Specify address and block ranges for best performance. Returns at most ${MAX_LOGS} logs; past that, truncated is true and nextBlock says where to resume.`,
|
|
139
199
|
z.object({
|
|
140
200
|
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
|
|
141
201
|
eventAbi: z
|
|
142
202
|
.string()
|
|
143
203
|
.describe('Event ABI definition (e.g., "event Transfer(address indexed from, address indexed to, uint256 value)")'),
|
|
144
204
|
address: z.string().optional().describe('Contract address (RECOMMENDED for performance)'),
|
|
145
|
-
fromBlock: z.string().optional().describe('Start block (defaults to latest
|
|
146
|
-
toBlock: z.string().optional().describe('End block (defaults to latest)'),
|
|
205
|
+
fromBlock: z.string().optional().describe('Start block (defaults to the latest block)'),
|
|
206
|
+
toBlock: z.string().optional().describe('End block, inclusive (defaults to the latest block)'),
|
|
147
207
|
eventArgs: z
|
|
148
208
|
.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.union([z.string(), z.number(), z.boolean()]))]))
|
|
149
209
|
.optional()
|
|
150
|
-
.describe(
|
|
210
|
+
.describe(
|
|
211
|
+
'Filter by indexed parameters: {"from": "0x123...", "to": "0x456..."}. A list matches any of its values: {"to": ["0x1...", "0x2..."]}'
|
|
212
|
+
)
|
|
151
213
|
}),
|
|
152
214
|
async (args) => {
|
|
153
215
|
const clientManager = getClientManager()
|
|
154
|
-
const
|
|
216
|
+
const chain = args.chain as ChainName
|
|
217
|
+
const abiItem = parseAbiItem(args.eventAbi) as AbiEvent
|
|
155
218
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const abiItem = parseAbiItem(args.eventAbi) as AbiEvent
|
|
160
|
-
|
|
161
|
-
// Determine block range
|
|
162
|
-
const fromBlockNum = args.fromBlock ? BigInt(args.fromBlock) : undefined
|
|
163
|
-
const toBlockNum = args.toBlock ? BigInt(args.toBlock) : undefined
|
|
164
|
-
|
|
165
|
-
// Prepare getLogs parameters
|
|
166
|
-
const getLogsParams: {
|
|
167
|
-
address?: Address
|
|
168
|
-
event: AbiEvent
|
|
169
|
-
fromBlock?: bigint
|
|
170
|
-
toBlock?: bigint
|
|
171
|
-
args?: Record<string, unknown>
|
|
172
|
-
} = {
|
|
173
|
-
event: abiItem,
|
|
174
|
-
fromBlock: fromBlockNum,
|
|
175
|
-
toBlock: toBlockNum
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Add address filter if provided
|
|
179
|
-
if (args.address && isAddress(args.address)) {
|
|
180
|
-
getLogsParams.address = args.address as Address
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Add event argument filters if provided
|
|
184
|
-
if (args.eventArgs && Object.keys(args.eventArgs).length > 0) {
|
|
185
|
-
// Convert event arguments to proper types based on ABI
|
|
186
|
-
const convertedEventArgs = convertEventArgsToTypes(args.eventArgs, abiItem.inputs)
|
|
187
|
-
getLogsParams.args = convertedEventArgs
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Use client.getLogs with proper typing
|
|
191
|
-
const logs = await client.getLogs(getLogsParams)
|
|
192
|
-
|
|
193
|
-
// Convert logs to serializable format
|
|
194
|
-
const serializedLogs = logs.map((log) => {
|
|
195
|
-
const baseLogData = {
|
|
196
|
-
address: log.address,
|
|
197
|
-
blockHash: log.blockHash,
|
|
198
|
-
blockNumber: log.blockNumber?.toString(),
|
|
199
|
-
data: log.data,
|
|
200
|
-
logIndex: log.logIndex,
|
|
201
|
-
topics: log.topics,
|
|
202
|
-
transactionHash: log.transactionHash,
|
|
203
|
-
transactionIndex: log.transactionIndex
|
|
204
|
-
}
|
|
219
|
+
if (args.address && !isAddress(args.address)) {
|
|
220
|
+
throw new Error(`Invalid contract address: ${args.address}`)
|
|
221
|
+
}
|
|
205
222
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
})
|
|
223
|
+
const eventArgs =
|
|
224
|
+
args.eventArgs && Object.keys(args.eventArgs).length > 0
|
|
225
|
+
? convertEventArgsToTypes(args.eventArgs, abiItem.inputs)
|
|
226
|
+
: undefined
|
|
227
|
+
const fromBlockNum = args.fromBlock ? BigInt(args.fromBlock) : undefined
|
|
228
|
+
const toBlockNum = args.toBlock ? BigInt(args.toBlock) : undefined
|
|
214
229
|
|
|
230
|
+
const respond = (logs: DecodedLog[], dataSource: 'viem' | 'hypersync') => {
|
|
231
|
+
const capped = capLogs(logs)
|
|
215
232
|
return formatResponse({
|
|
216
|
-
|
|
233
|
+
...capped,
|
|
217
234
|
eventSignature: toEventSignature(abiItem),
|
|
218
235
|
chain: args.chain,
|
|
219
236
|
fromBlock: fromBlockNum?.toString() || 'latest',
|
|
220
237
|
toBlock: toBlockNum?.toString() || 'latest',
|
|
221
|
-
count: logs.length,
|
|
222
|
-
dataSource
|
|
238
|
+
count: capped.logs.length,
|
|
239
|
+
dataSource,
|
|
223
240
|
filters: {
|
|
224
241
|
address: args.address || null,
|
|
225
242
|
eventArgs: args.eventArgs || null
|
|
226
243
|
}
|
|
227
244
|
})
|
|
228
|
-
}
|
|
229
|
-
// If viem fails and we have hypersync support for this chain, try hypersync as fallback
|
|
230
|
-
if (clientManager.getHypersyncUrl(args.chain as ChainName)) {
|
|
231
|
-
console.warn(`Viem getLogs failed for ${args.chain}, trying hypersync fallback:`, viemError)
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const hypersyncLogs = await getLogsWithHypersync(
|
|
235
|
-
args.chain as ChainName,
|
|
236
|
-
args.eventAbi,
|
|
237
|
-
config.hypersyncApiKey,
|
|
238
|
-
args.address,
|
|
239
|
-
args.fromBlock,
|
|
240
|
-
args.toBlock,
|
|
241
|
-
args.eventArgs
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
const abiItem = parseAbiItem(args.eventAbi) as AbiEvent
|
|
245
|
-
const fromBlockNum = args.fromBlock ? BigInt(args.fromBlock) : undefined
|
|
246
|
-
const toBlockNum = args.toBlock ? BigInt(args.toBlock) : undefined
|
|
247
|
-
|
|
248
|
-
// Convert hypersync logs to the same format as viem logs
|
|
249
|
-
const serializedLogs = hypersyncLogs.map((log) => {
|
|
250
|
-
const baseLogData = {
|
|
251
|
-
address: log.address,
|
|
252
|
-
blockHash: log.blockHash,
|
|
253
|
-
blockNumber: log.blockNumber?.toString(),
|
|
254
|
-
data: log.data,
|
|
255
|
-
logIndex: log.logIndex,
|
|
256
|
-
topics: log.topics,
|
|
257
|
-
transactionHash: log.transactionHash,
|
|
258
|
-
transactionIndex: log.transactionIndex
|
|
259
|
-
}
|
|
245
|
+
}
|
|
260
246
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
247
|
+
try {
|
|
248
|
+
const logs = await clientManager.getClient(chain).getLogs({
|
|
249
|
+
address: args.address as Address | undefined,
|
|
250
|
+
event: abiItem,
|
|
251
|
+
args: eventArgs,
|
|
252
|
+
fromBlock: fromBlockNum,
|
|
253
|
+
toBlock: toBlockNum
|
|
254
|
+
})
|
|
255
|
+
return respond(logs as DecodedLog[], 'viem')
|
|
256
|
+
} catch (viemError) {
|
|
257
|
+
if (!clientManager.getHypersyncUrl(chain)) {
|
|
258
|
+
throw new Error(`Failed to get logs with viem (hypersync not available for ${args.chain}): ${rpcReason(viemError)}`)
|
|
259
|
+
}
|
|
260
|
+
console.warn(`Viem getLogs failed for ${args.chain}, trying hypersync fallback: ${rpcReason(viemError)}`)
|
|
269
261
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
} else {
|
|
287
|
-
// No hypersync fallback available for this chain
|
|
288
|
-
throw new Error(`Failed to get logs with viem (hypersync not available for ${args.chain}): ${viemError}`)
|
|
262
|
+
try {
|
|
263
|
+
const logs = await getLogsWithHypersync(
|
|
264
|
+
chain,
|
|
265
|
+
abiItem,
|
|
266
|
+
clientManager.getConfig().hypersyncApiKey,
|
|
267
|
+
args.address,
|
|
268
|
+
args.fromBlock,
|
|
269
|
+
args.toBlock,
|
|
270
|
+
eventArgs
|
|
271
|
+
)
|
|
272
|
+
return respond(logs, 'hypersync')
|
|
273
|
+
} catch (hypersyncError) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Both viem and hypersync failed. Viem error: ${rpcReason(viemError)}. Hypersync error: ${rpcReason(hypersyncError)}`
|
|
276
|
+
)
|
|
289
277
|
}
|
|
290
278
|
}
|
|
291
279
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AbiEvent,
|
|
3
3
|
type AbiFunction,
|
|
4
|
-
type AbiParameter,
|
|
5
4
|
encodeFunctionData,
|
|
6
5
|
keccak256,
|
|
7
6
|
parseAbiItem,
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
} from 'viem'
|
|
12
11
|
import { z } from 'zod'
|
|
13
12
|
import type { AbiError } from '../../types.js'
|
|
14
|
-
import { createTool, formatResponse } from '../../utils.js'
|
|
13
|
+
import { convertArgumentsToTypes, createTool, formatResponse } from '../../utils.js'
|
|
15
14
|
|
|
16
15
|
const FunctionAbiSchema = z.object({
|
|
17
16
|
functionAbi: z.string().describe('Function ABI definition (e.g., "function transfer(address to, uint256 amount)")')
|
|
@@ -92,7 +91,8 @@ export default {
|
|
|
92
91
|
async (args) => {
|
|
93
92
|
const results = args.items.map((item) => {
|
|
94
93
|
const abiItem = parseAbiItem(item.errorAbi) as AbiError
|
|
95
|
-
|
|
94
|
+
// Formatted as a function, which an error's signature is; viem keeps an "error " prefix otherwise.
|
|
95
|
+
const signature = toFunctionSignature({ ...abiItem, type: 'function', outputs: [], stateMutability: 'nonpayable' })
|
|
96
96
|
const hash = keccak256(toBytes(signature))
|
|
97
97
|
const selector = hash.slice(0, 10)
|
|
98
98
|
|
|
@@ -125,7 +125,7 @@ export default {
|
|
|
125
125
|
const data = encodeFunctionData({
|
|
126
126
|
abi: [abiItem],
|
|
127
127
|
functionName: abiItem.name,
|
|
128
|
-
args: args.args
|
|
128
|
+
args: convertArgumentsToTypes(args.args, abiItem.inputs)
|
|
129
129
|
})
|
|
130
130
|
|
|
131
131
|
return formatResponse({
|