web3-tools-mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +92 -259
  2. package/dist/client.d.ts +1653 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +142 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +78 -28274
  9. package/dist/index.js.map +1 -0
  10. package/dist/package.json +60 -0
  11. package/dist/src/client.d.ts +1653 -0
  12. package/dist/src/client.d.ts.map +1 -0
  13. package/dist/src/client.js +142 -0
  14. package/dist/src/client.js.map +1 -0
  15. package/dist/src/index.d.ts +3 -0
  16. package/dist/src/index.d.ts.map +1 -0
  17. package/dist/src/index.js +88 -0
  18. package/dist/src/index.js.map +1 -0
  19. package/dist/src/tools/advanced.d.ts +75 -0
  20. package/dist/src/tools/advanced.d.ts.map +1 -0
  21. package/dist/src/tools/advanced.js +234 -0
  22. package/dist/src/tools/advanced.js.map +1 -0
  23. package/dist/src/tools/balance.d.ts +55 -0
  24. package/dist/src/tools/balance.d.ts.map +1 -0
  25. package/dist/src/tools/balance.js +146 -0
  26. package/dist/src/tools/balance.js.map +1 -0
  27. package/dist/src/tools/contract-info.d.ts +79 -0
  28. package/dist/src/tools/contract-info.d.ts.map +1 -0
  29. package/dist/src/tools/contract-info.js +450 -0
  30. package/dist/src/tools/contract-info.js.map +1 -0
  31. package/dist/src/tools/contract.d.ts +79 -0
  32. package/dist/src/tools/contract.d.ts.map +1 -0
  33. package/dist/src/tools/contract.js +140 -0
  34. package/dist/src/tools/contract.js.map +1 -0
  35. package/dist/src/tools/index.d.ts +3 -0
  36. package/dist/src/tools/index.d.ts.map +1 -0
  37. package/dist/src/tools/index.js +27 -0
  38. package/dist/src/tools/index.js.map +1 -0
  39. package/dist/src/tools/logs.d.ts +39 -0
  40. package/dist/src/tools/logs.d.ts.map +1 -0
  41. package/dist/src/tools/logs.js +231 -0
  42. package/dist/src/tools/logs.js.map +1 -0
  43. package/dist/src/tools/signatures.d.ts +83 -0
  44. package/dist/src/tools/signatures.d.ts.map +1 -0
  45. package/dist/src/tools/signatures.js +93 -0
  46. package/dist/src/tools/signatures.js.map +1 -0
  47. package/dist/src/types.d.ts +22 -0
  48. package/dist/src/types.d.ts.map +1 -0
  49. package/dist/src/types.js +2 -0
  50. package/dist/src/types.js.map +1 -0
  51. package/dist/src/utils.d.ts +22 -0
  52. package/dist/src/utils.d.ts.map +1 -0
  53. package/dist/src/utils.js +183 -0
  54. package/dist/src/utils.js.map +1 -0
  55. package/dist/test/mcp-client.test.d.ts +2 -0
  56. package/dist/test/mcp-client.test.d.ts.map +1 -0
  57. package/dist/test/mcp-client.test.js +376 -0
  58. package/dist/test/mcp-client.test.js.map +1 -0
  59. package/dist/test/tools/advanced.test.d.ts +2 -0
  60. package/dist/test/tools/advanced.test.d.ts.map +1 -0
  61. package/dist/test/tools/advanced.test.js +235 -0
  62. package/dist/test/tools/advanced.test.js.map +1 -0
  63. package/dist/test/tools/balance.test.d.ts +2 -0
  64. package/dist/test/tools/balance.test.d.ts.map +1 -0
  65. package/dist/test/tools/balance.test.js +167 -0
  66. package/dist/test/tools/balance.test.js.map +1 -0
  67. package/dist/test/tools/contract-info.test.d.ts +2 -0
  68. package/dist/test/tools/contract-info.test.d.ts.map +1 -0
  69. package/dist/test/tools/contract-info.test.js +286 -0
  70. package/dist/test/tools/contract-info.test.js.map +1 -0
  71. package/dist/test/tools/contract.test.d.ts +2 -0
  72. package/dist/test/tools/contract.test.d.ts.map +1 -0
  73. package/dist/test/tools/contract.test.js +234 -0
  74. package/dist/test/tools/contract.test.js.map +1 -0
  75. package/dist/test/tools/logs.test.d.ts +2 -0
  76. package/dist/test/tools/logs.test.d.ts.map +1 -0
  77. package/dist/test/tools/logs.test.js +196 -0
  78. package/dist/test/tools/logs.test.js.map +1 -0
  79. package/dist/test/tools/signatures.test.d.ts +2 -0
  80. package/dist/test/tools/signatures.test.d.ts.map +1 -0
  81. package/dist/test/tools/signatures.test.js +112 -0
  82. package/dist/test/tools/signatures.test.js.map +1 -0
  83. package/dist/tools/advanced.d.ts +75 -0
  84. package/dist/tools/advanced.d.ts.map +1 -0
  85. package/dist/tools/advanced.js +234 -0
  86. package/dist/tools/advanced.js.map +1 -0
  87. package/dist/tools/balance.d.ts +55 -0
  88. package/dist/tools/balance.d.ts.map +1 -0
  89. package/dist/tools/balance.js +146 -0
  90. package/dist/tools/balance.js.map +1 -0
  91. package/dist/tools/batch.d.ts +122 -0
  92. package/dist/tools/batch.d.ts.map +1 -0
  93. package/dist/tools/batch.js +243 -0
  94. package/dist/tools/batch.js.map +1 -0
  95. package/dist/tools/contract-info.d.ts +79 -0
  96. package/dist/tools/contract-info.d.ts.map +1 -0
  97. package/dist/tools/contract-info.js +450 -0
  98. package/dist/tools/contract-info.js.map +1 -0
  99. package/dist/tools/contract.d.ts +79 -0
  100. package/dist/tools/contract.d.ts.map +1 -0
  101. package/dist/tools/contract.js +140 -0
  102. package/dist/tools/contract.js.map +1 -0
  103. package/dist/tools/index.d.ts +3 -0
  104. package/dist/tools/index.d.ts.map +1 -0
  105. package/dist/tools/index.js +27 -0
  106. package/dist/tools/index.js.map +1 -0
  107. package/dist/tools/logs.d.ts +39 -0
  108. package/dist/tools/logs.d.ts.map +1 -0
  109. package/dist/tools/logs.js +231 -0
  110. package/dist/tools/logs.js.map +1 -0
  111. package/dist/tools/signatures.d.ts +83 -0
  112. package/dist/tools/signatures.d.ts.map +1 -0
  113. package/dist/tools/signatures.js +93 -0
  114. package/dist/tools/signatures.js.map +1 -0
  115. package/dist/types.d.ts +22 -0
  116. package/dist/types.d.ts.map +1 -0
  117. package/dist/types.js +2 -0
  118. package/dist/types.js.map +1 -0
  119. package/dist/utils.d.ts +22 -0
  120. package/dist/utils.d.ts.map +1 -0
  121. package/dist/utils.js +183 -0
  122. package/dist/utils.js.map +1 -0
  123. package/package.json +35 -23
  124. package/src/client.ts +160 -0
  125. package/src/index.ts +96 -0
  126. package/src/tools/advanced.ts +262 -0
  127. package/src/tools/balance.ts +176 -0
  128. package/src/tools/contract-info.ts +582 -0
  129. package/src/tools/contract.ts +180 -0
  130. package/src/tools/index.ts +35 -0
  131. package/src/tools/logs.ts +291 -0
  132. package/src/tools/signatures.ts +126 -0
  133. package/src/types.ts +29 -0
  134. package/src/utils.ts +222 -0
  135. package/dist/hypersync-client.darwin-arm64-7fegfwn5.node +0 -0
package/src/index.ts ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ import packageJson from "../package.json" with { type: "json" };
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { initializeClientManager } from "./client.js";
7
+ import { registerAllTools } from "./tools/index.js";
8
+ import { parseCommandLineArgs } from "./utils.js";
9
+
10
+ // Parse configuration
11
+ const config = parseCommandLineArgs();
12
+
13
+ // Show help if requested
14
+ if (config.showHelp) {
15
+ console.log(`
16
+ Web3 Tools MCP Server v${packageJson.version}
17
+ ${packageJson.description}
18
+
19
+ USAGE:
20
+ npx web3-tools-mcp [OPTIONS]
21
+
22
+ OPTIONS:
23
+ --help, -h Show this help message
24
+ --etherscan-api-key <key> Etherscan API key (for contract ABI retrieval)
25
+ --alchemy-api-key <key> Alchemy API key (for enhanced RPC)
26
+ --infura-api-key <key> Infura API key (for alternative RPC)
27
+ --hypersync-api-key <key> Hypersync API key (for fast event queries)
28
+ --custom-rpc <json> Custom RPC URLs as JSON object
29
+ Example: '{"mainnet":"https://...", "base":"https://..."}'
30
+
31
+ ENVIRONMENT VARIABLES:
32
+ ETHERSCAN_API_KEY Alternative to --etherscan-api-key
33
+ ALCHEMY_API_KEY Alternative to --alchemy-api-key
34
+ INFURA_API_KEY Alternative to --infura-api-key
35
+ HYPERSYNC_API_KEY Alternative to --hypersync-api-key
36
+
37
+ SUPPORTED CHAINS:
38
+ mainnet, base, arbitrum, polygon, optimism, celo, localhost
39
+
40
+ EXAMPLES:
41
+ # Use with npx (recommended)
42
+ npx web3-tools-mcp --etherscan-api-key YOUR_KEY
43
+
44
+ # Use environment variables
45
+ export ETHERSCAN_API_KEY=your_key
46
+ npx web3-tools-mcp
47
+
48
+ # Use custom RPC
49
+ npx web3-tools-mcp --custom-rpc '{"mainnet":"https://my-rpc.com"}'
50
+
51
+ DOCUMENTATION:
52
+ GitHub: ${packageJson.repository.url.replace('git+', '').replace('.git', '')}
53
+ Issues: ${packageJson.bugs.url}
54
+
55
+ For MCP client configuration, see the README.md file.
56
+ `);
57
+ process.exit(0);
58
+ }
59
+
60
+ // Log configuration info
61
+ if (config.etherscanApiKey) {
62
+ console.error("[MCP] Etherscan API key configured");
63
+ }
64
+ if (config.alchemyApiKey) {
65
+ console.error("[MCP] Alchemy API key configured");
66
+ }
67
+ if (config.infuraApiKey) {
68
+ console.error("[MCP] Infura API key configured");
69
+ }
70
+ if (config.customRpcUrls) {
71
+ console.error("[MCP] Custom RPC URLs:", Object.keys(config.customRpcUrls));
72
+ }
73
+
74
+ // Initialize client manager
75
+ initializeClientManager(config);
76
+
77
+ // Create MCP server instance
78
+ const server = new McpServer({
79
+ name: "web3-tools-mcp",
80
+ version: packageJson.version,
81
+ });
82
+
83
+ // Register all tools
84
+ registerAllTools(server);
85
+
86
+ // Start server
87
+ async function main() {
88
+ const transport = new StdioServerTransport();
89
+ await server.connect(transport);
90
+ console.error("Web3 Tools MCP Server running on stdio");
91
+ }
92
+
93
+ main().catch((error) => {
94
+ console.error("Fatal error:", error);
95
+ process.exit(1);
96
+ });
@@ -0,0 +1,262 @@
1
+ import { type Address, decodeAbiParameters, isAddress, parseAbiParameters } from 'viem'
2
+ import { z } from 'zod'
3
+ import type { ChainName } from '../types.js'
4
+ import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
5
+ import { createTool, formatResponse } from '../utils.js'
6
+
7
+ export default {
8
+ get_storage_at: createTool(
9
+ 'Read Contract Storage',
10
+ '⚠️ ADVANCED: Direct storage slot access with type decoding. Use for low-level contract inspection.',
11
+ z.object({
12
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
13
+ address: z.string().describe('The contract address to inspect'),
14
+ slot: z.string().describe('Storage slot (hex string, e.g., "0x0")'),
15
+ abiType: z
16
+ .string()
17
+ .describe('ABI type for decoding (e.g., "uint256", "address", "bool", "bytes32")'),
18
+ blockNumber: z.string().optional().describe('Block number (optional, defaults to latest)')
19
+ }),
20
+ async (args) => {
21
+ if (!isAddress(args.address)) {
22
+ throw new Error('Invalid address format')
23
+ }
24
+
25
+ const clientManager = getClientManager()
26
+ const client = clientManager.getClient(args.chain as ChainName)
27
+ const blockTag = args.blockNumber ? BigInt(args.blockNumber) : 'latest'
28
+
29
+ try {
30
+ const storageValue = await client.getStorageAt({
31
+ address: args.address as Address,
32
+ slot: args.slot as `0x${string}`,
33
+ blockNumber: blockTag === 'latest' ? undefined : blockTag
34
+ })
35
+
36
+ if (!storageValue) {
37
+ throw new Error('No storage value found')
38
+ }
39
+
40
+ // Decode based on ABI type
41
+ let decodedValue: unknown
42
+ let formattedValue: string
43
+
44
+ try {
45
+ switch (args.abiType.toLowerCase()) {
46
+ case 'uint256':
47
+ case 'uint': {
48
+ decodedValue = BigInt(storageValue)
49
+ formattedValue = (decodedValue as bigint).toString()
50
+ break
51
+ }
52
+ case 'int256':
53
+ case 'int': {
54
+ // Handle two's complement for negative numbers
55
+ const uint256Value = BigInt(storageValue)
56
+ const maxUint256 = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
57
+ decodedValue = uint256Value > maxUint256 / 2n ? uint256Value - maxUint256 - 1n : uint256Value
58
+ formattedValue = (decodedValue as bigint).toString()
59
+ break
60
+ }
61
+ case 'address': {
62
+ decodedValue = `0x${storageValue.slice(-40)}`
63
+ formattedValue = decodedValue as string
64
+ break
65
+ }
66
+ case 'bool': {
67
+ decodedValue = BigInt(storageValue) !== 0n
68
+ formattedValue = String(decodedValue)
69
+ break
70
+ }
71
+ case 'bytes32': {
72
+ decodedValue = storageValue
73
+ formattedValue = storageValue
74
+ break
75
+ }
76
+ default: {
77
+ // Try to decode as ABI parameters
78
+ try {
79
+ const parsedTypes = parseAbiParameters([args.abiType])
80
+ if (parsedTypes.length > 0) {
81
+ const decoded = decodeAbiParameters(parsedTypes, storageValue) as readonly unknown[]
82
+ decodedValue = decoded.length > 0 ? decoded[0] : null
83
+ formattedValue = decodedValue ? String(decodedValue) : 'No data'
84
+ } else {
85
+ decodedValue = null
86
+ formattedValue = 'Invalid ABI type'
87
+ }
88
+ } catch {
89
+ decodedValue = null
90
+ formattedValue = 'Failed to decode with custom ABI type'
91
+ }
92
+ break
93
+ }
94
+ }
95
+ } catch {
96
+ decodedValue = null
97
+ formattedValue = 'Failed to decode'
98
+ }
99
+
100
+ return formatResponse({
101
+ rawValue: storageValue,
102
+ decodedValue,
103
+ formattedValue,
104
+ abiType: args.abiType,
105
+ slot: args.slot,
106
+ address: args.address,
107
+ chain: args.chain,
108
+ blockNumber: blockTag === 'latest' ? 'latest' : blockTag.toString()
109
+ })
110
+ } catch (error) {
111
+ throw new Error(`Failed to get storage: ${error}`)
112
+ }
113
+ }
114
+ ),
115
+
116
+ get_block_info: createTool(
117
+ 'Get Block Information',
118
+ 'Retrieve block data including timestamps, hashes, and dates. Use for time-based analysis.',
119
+ z.object({
120
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
121
+ blockNumber: z.string().optional().describe('Block number (optional, defaults to latest)')
122
+ }),
123
+ async (args) => {
124
+ const clientManager = getClientManager()
125
+ const client = clientManager.getClient(args.chain as ChainName)
126
+ const blockTag = args.blockNumber ? BigInt(args.blockNumber) : 'latest'
127
+
128
+ try {
129
+ const block = await client.getBlock({
130
+ blockNumber: blockTag === 'latest' ? undefined : blockTag,
131
+ includeTransactions: false
132
+ })
133
+
134
+ const timestamp = Number(block.timestamp)
135
+ const date = new Date(timestamp * 1000)
136
+
137
+ return formatResponse({
138
+ blockNumber: block.number,
139
+ timestamp: block.timestamp,
140
+ timestampMs: timestamp * 1000,
141
+ dateIso: date.toISOString(),
142
+ dateReadable: date.toLocaleString(),
143
+ hash: block.hash,
144
+ parentHash: block.parentHash,
145
+ chain: args.chain
146
+ })
147
+ } catch (error) {
148
+ throw new Error(`Failed to get block time: ${error}`)
149
+ }
150
+ }
151
+ ),
152
+
153
+ trace_transaction: createTool(
154
+ 'Trace Transaction',
155
+ '⚠️ INTENSIVE: Detailed transaction analysis including call traces and state changes. Use sparingly.',
156
+ z.object({
157
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
158
+ transactionHash: z.string().describe('Transaction hash to trace (0x prefixed)'),
159
+ traceType: z
160
+ .enum(['trace', 'vmTrace', 'stateDiff'])
161
+ .optional()
162
+ .describe(
163
+ 'Trace type: "trace" (call tree, recommended), "vmTrace" (VM execution), "stateDiff" (state changes)'
164
+ )
165
+ .default('trace')
166
+ }),
167
+ async (args) => {
168
+ const clientManager = getClientManager()
169
+ const client = clientManager.getClient(args.chain as ChainName)
170
+
171
+ try {
172
+ // First get the transaction receipt to make sure it exists
173
+ const transaction = await client.getTransaction({ hash: args.transactionHash as `0x${string}` })
174
+ const receipt = await client.getTransactionReceipt({ hash: args.transactionHash as `0x${string}` })
175
+
176
+ let traceResult: unknown = null
177
+
178
+ // Perform the requested trace type
179
+ switch (args.traceType) {
180
+ case 'trace':
181
+ try {
182
+ // Use debug_traceTransaction for call trace
183
+ traceResult = await client.request({
184
+ method: 'debug_traceTransaction',
185
+ params: [args.transactionHash, { tracer: 'callTracer' }]
186
+ })
187
+ } catch (e) {
188
+ traceResult = { error: (e as Error).message }
189
+ }
190
+ break
191
+
192
+ case 'vmTrace':
193
+ try {
194
+ traceResult = await client.request({
195
+ method: 'debug_traceTransaction',
196
+ params: [args.transactionHash, { tracer: 'prestateTracer' }]
197
+ })
198
+ } catch (e) {
199
+ traceResult = { error: (e as Error).message }
200
+ }
201
+ break
202
+
203
+ case 'stateDiff':
204
+ try {
205
+ traceResult = await client.request({
206
+ method: 'debug_traceTransaction',
207
+ params: [args.transactionHash, { tracer: 'stateDiffTracer' }]
208
+ })
209
+ } catch (e) {
210
+ traceResult = { error: (e as Error).message }
211
+ }
212
+ break
213
+
214
+ default:
215
+ traceResult = {
216
+ type: 'CALL',
217
+ from: transaction.from,
218
+ to: transaction.to,
219
+ value: transaction.value?.toString() || '0',
220
+ gas: transaction.gas?.toString() || '0',
221
+ gasUsed: receipt.gasUsed?.toString() || '0',
222
+ input: transaction.input,
223
+ output: '0x',
224
+ error: receipt.status === 'success' ? null : 'Transaction failed'
225
+ }
226
+ }
227
+
228
+ const result = {
229
+ success: true,
230
+ chain: args.chain,
231
+ transactionHash: args.transactionHash,
232
+ traceType: args.traceType,
233
+ transaction: {
234
+ blockNumber: transaction.blockNumber?.toString(),
235
+ from: transaction.from,
236
+ to: transaction.to,
237
+ value: transaction.value?.toString() || '0',
238
+ gas: transaction.gas?.toString() || '0',
239
+ gasPrice: transaction.gasPrice?.toString() || '0',
240
+ nonce: transaction.nonce?.toString() || '0',
241
+ input: transaction.input
242
+ },
243
+ receipt: {
244
+ status: receipt.status,
245
+ gasUsed: receipt.gasUsed?.toString() || '0',
246
+ effectiveGasPrice: receipt.effectiveGasPrice?.toString() || '0',
247
+ logs: receipt.logs.map(log => ({
248
+ address: log.address,
249
+ topics: log.topics,
250
+ data: log.data
251
+ }))
252
+ },
253
+ trace: traceResult
254
+ }
255
+
256
+ return formatResponse(result)
257
+ } catch (error) {
258
+ throw new Error(`Transaction trace failed: ${error}`)
259
+ }
260
+ }
261
+ )
262
+ }
@@ -0,0 +1,176 @@
1
+ import { type Address, formatEther, formatUnits, isAddress } from 'viem'
2
+ import { z } from 'zod'
3
+ import type { ChainName } from '../types.js'
4
+ import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
5
+ import { createTool, formatResponse } from '../utils.js'
6
+
7
+ const BalanceQuerySchema = z.object({
8
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
9
+ address: z.string().describe('The address to check balance for'),
10
+ tokenAddress: z
11
+ .string()
12
+ .optional()
13
+ .describe('Token contract address (optional, if not provided returns native token balance)'),
14
+ blockNumber: z.string().optional().describe('Block number to query (optional, defaults to latest)'),
15
+ label: z.string().optional().describe('Optional label for batch results')
16
+ })
17
+
18
+ export default {
19
+ get_balance: createTool(
20
+ 'Get Token Balances',
21
+ 'Retrieve native and ERC20 token balances for multiple addresses. BATCH OPTIMIZED - use for portfolio tracking, wallet monitoring.',
22
+ z.object({
23
+ queries: z.array(BalanceQuerySchema).describe('Array of balance queries. Omit tokenAddress for native balance, include for ERC20.')
24
+ }),
25
+ async (args) => {
26
+ // Group by chain and blockNumber for efficient processing
27
+ const groupedQueries = new Map<string, typeof args.queries>()
28
+ for (const item of args.queries) {
29
+ const key = `${item.chain}:${item.blockNumber || 'latest'}`
30
+ if (!groupedQueries.has(key)) {
31
+ groupedQueries.set(key, [])
32
+ }
33
+ groupedQueries.get(key)!.push(item)
34
+ }
35
+
36
+ const allResults: Array<{
37
+ index: number
38
+ label?: string
39
+ address: string
40
+ tokenAddress?: string
41
+ chain: string
42
+ blockNumber: string
43
+ success: boolean
44
+ balance?: unknown
45
+ balanceFormatted?: string
46
+ decimals?: number
47
+ error?: string
48
+ }> = []
49
+
50
+ try {
51
+ // Process each group
52
+ for (const [key, queries] of groupedQueries) {
53
+ const [chain, blockNum] = key.split(':')
54
+ const clientManager = getClientManager()
55
+ const client = clientManager.getClient(chain as ChainName)
56
+ const blockTag = blockNum === 'latest' ? 'latest' : BigInt(blockNum)
57
+
58
+ // Process all queries in this group
59
+ const results = await Promise.allSettled(
60
+ queries.map(async (query) => {
61
+ if (!isAddress(query.address)) {
62
+ throw new Error(`Invalid address format: ${query.address}`)
63
+ }
64
+
65
+ const originalIndex = args.queries.indexOf(query)
66
+
67
+ // Native token balance
68
+ if (!query.tokenAddress) {
69
+ const balance = await client.getBalance({
70
+ address: query.address as Address,
71
+ blockNumber: blockTag === 'latest' ? undefined : blockTag
72
+ })
73
+
74
+ return {
75
+ index: originalIndex,
76
+ label: query.label,
77
+ address: query.address,
78
+ chain: query.chain,
79
+ blockNumber: query.blockNumber || 'latest',
80
+ success: true,
81
+ balance,
82
+ balanceFormatted: formatEther(balance)
83
+ }
84
+ }
85
+
86
+ // ERC20 token balance
87
+ if (!isAddress(query.tokenAddress)) {
88
+ throw new Error(`Invalid token address format: ${query.tokenAddress}`)
89
+ }
90
+
91
+ const balance = await client.readContract({
92
+ address: query.tokenAddress as Address,
93
+ abi: [
94
+ {
95
+ name: 'balanceOf',
96
+ type: 'function',
97
+ stateMutability: 'view',
98
+ inputs: [{ name: 'account', type: 'address' }],
99
+ outputs: [{ name: '', type: 'uint256' }]
100
+ }
101
+ ],
102
+ functionName: 'balanceOf',
103
+ args: [query.address as Address],
104
+ blockNumber: blockTag === 'latest' ? undefined : blockTag
105
+ })
106
+
107
+ // Try to get decimals
108
+ let decimals = 18
109
+ try {
110
+ decimals = (await client.readContract({
111
+ address: query.tokenAddress as Address,
112
+ abi: [
113
+ {
114
+ name: 'decimals',
115
+ type: 'function',
116
+ stateMutability: 'view',
117
+ inputs: [],
118
+ outputs: [{ name: '', type: 'uint8' }]
119
+ }
120
+ ],
121
+ functionName: 'decimals',
122
+ blockNumber: blockTag === 'latest' ? undefined : blockTag
123
+ })) as number
124
+ } catch {
125
+ // Use default decimals if call fails
126
+ }
127
+
128
+ return {
129
+ index: originalIndex,
130
+ label: query.label,
131
+ address: query.address,
132
+ tokenAddress: query.tokenAddress,
133
+ chain: query.chain,
134
+ blockNumber: query.blockNumber || 'latest',
135
+ success: true,
136
+ balance,
137
+ balanceFormatted: formatUnits(balance as bigint, decimals),
138
+ decimals
139
+ }
140
+ })
141
+ )
142
+
143
+ // Process results
144
+ results.forEach((result, idx) => {
145
+ if (result.status === 'fulfilled') {
146
+ allResults[result.value.index] = result.value
147
+ } else {
148
+ const query = queries[idx]!
149
+ const originalIndex = args.queries.indexOf(query)
150
+ allResults[originalIndex] = {
151
+ index: originalIndex,
152
+ label: query.label,
153
+ address: query.address,
154
+ tokenAddress: query.tokenAddress,
155
+ chain: query.chain,
156
+ blockNumber: query.blockNumber || 'latest',
157
+ success: false,
158
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
159
+ }
160
+ }
161
+ })
162
+ }
163
+
164
+ return formatResponse({
165
+ success: true,
166
+ totalQueries: args.queries.length,
167
+ successfulQueries: allResults.filter((r) => r.success).length,
168
+ failedQueries: allResults.filter((r) => !r.success).length,
169
+ results: allResults
170
+ })
171
+ } catch (error) {
172
+ throw new Error(`Balance query failed: ${error}`)
173
+ }
174
+ }
175
+ )
176
+ }