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.
Files changed (113) hide show
  1. package/CHANGELOG.md +115 -2
  2. package/README.md +96 -60
  3. package/dist/chain-meta.d.ts.map +1 -1
  4. package/dist/chain-meta.js +19 -8
  5. package/dist/chain-meta.js.map +1 -1
  6. package/dist/chains.d.ts +609 -65
  7. package/dist/chains.d.ts.map +1 -1
  8. package/dist/chains.js +89 -28
  9. package/dist/chains.js.map +1 -1
  10. package/dist/clear-signing.d.ts.map +1 -1
  11. package/dist/clear-signing.js +17 -6
  12. package/dist/clear-signing.js.map +1 -1
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +2 -4
  15. package/dist/client.js.map +1 -1
  16. package/dist/http-server.d.ts +1 -1
  17. package/dist/http-server.d.ts.map +1 -1
  18. package/dist/http-server.js +12 -5
  19. package/dist/http-server.js.map +1 -1
  20. package/dist/index.js +5 -6
  21. package/dist/index.js.map +1 -1
  22. package/dist/kv-storage.d.ts +2 -1
  23. package/dist/kv-storage.d.ts.map +1 -1
  24. package/dist/kv-storage.js +5 -2
  25. package/dist/kv-storage.js.map +1 -1
  26. package/dist/oauth.d.ts +18 -4
  27. package/dist/oauth.d.ts.map +1 -1
  28. package/dist/oauth.js +62 -26
  29. package/dist/oauth.js.map +1 -1
  30. package/dist/preview.d.ts.map +1 -1
  31. package/dist/preview.js +7 -5
  32. package/dist/preview.js.map +1 -1
  33. package/dist/tools/index.d.ts.map +1 -1
  34. package/dist/tools/index.js +16 -1
  35. package/dist/tools/index.js.map +1 -1
  36. package/dist/tools/read/advanced.d.ts +25 -20
  37. package/dist/tools/read/advanced.d.ts.map +1 -1
  38. package/dist/tools/read/advanced.js +45 -33
  39. package/dist/tools/read/advanced.js.map +1 -1
  40. package/dist/tools/read/balance.d.ts +6 -6
  41. package/dist/tools/read/balance.d.ts.map +1 -1
  42. package/dist/tools/read/balance.js +17 -32
  43. package/dist/tools/read/balance.js.map +1 -1
  44. package/dist/tools/read/contract-info.d.ts +14 -12
  45. package/dist/tools/read/contract-info.d.ts.map +1 -1
  46. package/dist/tools/read/contract-info.js +53 -122
  47. package/dist/tools/read/contract-info.js.map +1 -1
  48. package/dist/tools/read/contract.d.ts +10 -10
  49. package/dist/tools/read/contract.d.ts.map +1 -1
  50. package/dist/tools/read/contract.js +2 -1
  51. package/dist/tools/read/contract.js.map +1 -1
  52. package/dist/tools/read/defi.d.ts +86 -0
  53. package/dist/tools/read/defi.d.ts.map +1 -0
  54. package/dist/tools/read/defi.js +210 -0
  55. package/dist/tools/read/defi.js.map +1 -0
  56. package/dist/tools/read/ens.d.ts +20 -40
  57. package/dist/tools/read/ens.d.ts.map +1 -1
  58. package/dist/tools/read/ens.js +25 -33
  59. package/dist/tools/read/ens.js.map +1 -1
  60. package/dist/tools/read/gas.d.ts +16 -16
  61. package/dist/tools/read/gas.d.ts.map +1 -1
  62. package/dist/tools/read/gas.js +13 -48
  63. package/dist/tools/read/gas.js.map +1 -1
  64. package/dist/tools/read/logs.d.ts +65 -4
  65. package/dist/tools/read/logs.d.ts.map +1 -1
  66. package/dist/tools/read/logs.js +96 -115
  67. package/dist/tools/read/logs.js.map +1 -1
  68. package/dist/tools/read/signatures.d.ts.map +1 -1
  69. package/dist/tools/read/signatures.js +4 -3
  70. package/dist/tools/read/signatures.js.map +1 -1
  71. package/dist/tools/read/signing.d.ts.map +1 -1
  72. package/dist/tools/read/signing.js +8 -2
  73. package/dist/tools/read/signing.js.map +1 -1
  74. package/dist/tools/sign/transactions.d.ts +16 -16
  75. package/dist/tools/sign/transactions.d.ts.map +1 -1
  76. package/dist/tools/sign/transactions.js +54 -9
  77. package/dist/tools/sign/transactions.js.map +1 -1
  78. package/dist/tools/sign/wallet.d.ts +4 -4
  79. package/dist/types.d.ts +0 -1
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/utils.d.ts +18 -0
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +43 -2
  84. package/dist/utils.js.map +1 -1
  85. package/dist/wallet-client.d.ts +1 -0
  86. package/dist/wallet-client.d.ts.map +1 -1
  87. package/dist/wallet-client.js +18 -5
  88. package/dist/wallet-client.js.map +1 -1
  89. package/package.json +4 -7
  90. package/src/chain-meta.ts +16 -7
  91. package/src/chains.ts +101 -31
  92. package/src/clear-signing.ts +15 -9
  93. package/src/client.ts +2 -3
  94. package/src/http-server.ts +11 -6
  95. package/src/index.ts +5 -6
  96. package/src/kv-storage.ts +6 -3
  97. package/src/oauth.ts +93 -34
  98. package/src/preview.ts +7 -5
  99. package/src/tools/index.ts +14 -1
  100. package/src/tools/read/advanced.ts +63 -34
  101. package/src/tools/read/balance.ts +18 -33
  102. package/src/tools/read/contract-info.ts +64 -144
  103. package/src/tools/read/contract.ts +2 -1
  104. package/src/tools/read/defi.ts +287 -0
  105. package/src/tools/read/ens.ts +26 -33
  106. package/src/tools/read/gas.ts +13 -50
  107. package/src/tools/read/logs.ts +131 -143
  108. package/src/tools/read/signatures.ts +4 -4
  109. package/src/tools/read/signing.ts +8 -2
  110. package/src/tools/sign/transactions.ts +60 -9
  111. package/src/types.ts +0 -1
  112. package/src/utils.ts +47 -2
  113. package/src/wallet-client.ts +26 -5
@@ -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, SUPPORTED_CHAINS } from '../../client.js'
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: z
164
- .enum(SUPPORTED_CHAINS)
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: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
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: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
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: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
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 URI for an ENS name. Returns the avatar URL if set.',
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: z.enum(SUPPORTED_CHAINS).default('mainnet').describe('Blockchain network (defaults to mainnet)'),
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
- avatar = await client.getEnsAvatar({
351
- name: normalizedName,
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
 
@@ -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 blockTag = args.blockNumber ? BigInt(args.blockNumber) : undefined
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: blockTag
239
- })
221
+ blockNumber: args.blockNumber ? BigInt(args.blockNumber) : undefined
222
+ }
240
223
 
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
- })
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: errorMessage,
283
- reverted: errorMessage.includes('revert') || errorMessage.includes('execution 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(MAX_BUNDLE)
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
- ? `${(Number(estimatedCost) / 1e18).toFixed(6)} ETH`
436
+ ? `${formatEther(estimatedCost)} ${client.chain?.nativeCurrency.symbol ?? 'ETH'}`
474
437
  : estimatedCost.toString()
475
438
  }
476
439
 
@@ -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
- eventAbi: string,
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 abiItem = parseAbiItem(eventAbi) as AbiEvent
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: (string | string[] | null)[] = [topic0]
115
+ const topics: (Hex | Hex[] | null)[] = [topic0]
62
116
 
63
117
  // Add indexed parameter filtering if eventArgs provided
64
- if (eventArgs && Object.keys(eventArgs).length > 0) {
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 : toTopic(argValue))
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
- fromBlock: fromBlock ? Number.parseInt(fromBlock, 10) : 0,
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
- const res = await client.get(query)
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
- // Convert hypersync logs to viem-compatible format
106
- const decodedLogs = res.data.logs
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) : 0n,
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 NonNullable<typeof log> => log !== null)
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
- 'Search and decode contract events with filtering. Uses viem with hypersync fallback. Specify address and block ranges for best performance.',
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-1000)'),
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('Filter by indexed parameters: {"from": "0x123...", "to": "0x456..."}')
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 config = (clientManager as any).config
216
+ const chain = args.chain as ChainName
217
+ const abiItem = parseAbiItem(args.eventAbi) as AbiEvent
155
218
 
156
- // First try with regular viem client
157
- try {
158
- const client = clientManager.getClient(args.chain as ChainName)
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
- return {
207
- ...baseLogData,
208
- decoded: {
209
- eventName: log.eventName,
210
- args: log.args
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
- logs: serializedLogs,
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: 'viem',
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
- } catch (viemError) {
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
- return {
262
- ...baseLogData,
263
- decoded: {
264
- eventName: log.eventName,
265
- args: log.args
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
- return formatResponse({
271
- logs: serializedLogs,
272
- eventSignature: toEventSignature(abiItem),
273
- chain: args.chain,
274
- fromBlock: fromBlockNum?.toString() || 'latest',
275
- toBlock: toBlockNum?.toString() || 'latest',
276
- count: hypersyncLogs.length,
277
- dataSource: 'hypersync',
278
- filters: {
279
- address: args.address || null,
280
- eventArgs: args.eventArgs || null
281
- }
282
- })
283
- } catch (hypersyncError) {
284
- throw new Error(`Both viem and hypersync failed. Viem error: ${viemError}. Hypersync error: ${hypersyncError}`)
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
- const signature = `${abiItem.name}(${abiItem.inputs.map((input: AbiParameter) => input.type).join(',')})`
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 as readonly unknown[]
128
+ args: convertArgumentsToTypes(args.args, abiItem.inputs)
129
129
  })
130
130
 
131
131
  return formatResponse({