web3-tools-mcp 1.3.4 → 1.4.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 (53) hide show
  1. package/README.md +51 -4
  2. package/dist/anvil.d.ts +34 -0
  3. package/dist/anvil.d.ts.map +1 -0
  4. package/dist/anvil.js +254 -0
  5. package/dist/anvil.js.map +1 -0
  6. package/dist/client.d.ts +3 -0
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +11 -0
  9. package/dist/client.js.map +1 -1
  10. package/dist/index.js +25 -5
  11. package/dist/index.js.map +1 -1
  12. package/dist/preview.d.ts +55 -0
  13. package/dist/preview.d.ts.map +1 -0
  14. package/dist/preview.js +233 -0
  15. package/dist/preview.js.map +1 -0
  16. package/dist/tools/advanced.d.ts +67 -9
  17. package/dist/tools/advanced.d.ts.map +1 -1
  18. package/dist/tools/advanced.js +206 -67
  19. package/dist/tools/advanced.js.map +1 -1
  20. package/dist/tools/balance.d.ts +5 -5
  21. package/dist/tools/contract-info.d.ts +9 -9
  22. package/dist/tools/contract.d.ts +8 -8
  23. package/dist/tools/ens.d.ts +15 -15
  24. package/dist/tools/gas.d.ts +18 -18
  25. package/dist/tools/logs.d.ts +3 -3
  26. package/dist/tools/transactions.d.ts +9 -9
  27. package/dist/tools/transactions.d.ts.map +1 -1
  28. package/dist/tools/transactions.js +92 -113
  29. package/dist/tools/transactions.js.map +1 -1
  30. package/dist/utils.d.ts +13 -0
  31. package/dist/utils.d.ts.map +1 -1
  32. package/dist/utils.js +76 -0
  33. package/dist/utils.js.map +1 -1
  34. package/dist/wallet-client.d.ts +83 -0
  35. package/dist/wallet-client.d.ts.map +1 -0
  36. package/dist/wallet-client.js +357 -0
  37. package/dist/wallet-client.js.map +1 -0
  38. package/package.json +9 -8
  39. package/src/anvil.ts +323 -0
  40. package/src/client.ts +14 -0
  41. package/src/index.ts +26 -5
  42. package/src/preview.ts +320 -0
  43. package/src/tools/advanced.ts +239 -70
  44. package/src/tools/transactions.ts +98 -125
  45. package/src/utils.ts +95 -0
  46. package/src/wallet-client.ts +380 -0
  47. package/dist/wallet-server.d.ts +0 -35
  48. package/dist/wallet-server.d.ts.map +0 -1
  49. package/dist/wallet-server.js +0 -232
  50. package/dist/wallet-server.js.map +0 -1
  51. package/public/wallet-app.js +0 -677
  52. package/public/wallet.html +0 -723
  53. package/src/wallet-server.ts +0 -283
@@ -1,18 +1,59 @@
1
1
  import { z } from 'zod'
2
- import { getWalletServer } from '../wallet-server.js'
3
- import { SUPPORTED_CHAINS } from '../client.js'
4
- import { parseUnits, encodeFunctionData, parseAbiItem, type AbiFunction } from 'viem'
5
- import { randomBytes } from 'crypto'
2
+ import { getWalletClient } from '../wallet-client.js'
3
+ import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
4
+ import { encodeFunctionData, isAddress, parseAbiItem, parseUnits, type AbiFunction, type Address } from 'viem'
5
+ import { randomBytes } from 'node:crypto'
6
+ import type { ChainName } from '../types.js'
7
+ import { buildTxPreview, type RawTx } from '../preview.js'
6
8
  import { createTool, formatResponse } from '../utils.js'
7
9
 
8
10
  function generateRequestId(): string {
9
11
  return randomBytes(16).toString('hex')
10
12
  }
11
13
 
14
+ function requireAddress(label: string, address: string): Address {
15
+ if (!isAddress(address)) throw new Error(`Invalid ${label}: ${address}`)
16
+ return address
17
+ }
18
+
19
+ function explorerTxUrl(chain: ChainName, txHash: unknown): string {
20
+ return `https://${getClientManager().getEtherscanDomain(chain)}/tx/${txHash}`
21
+ }
22
+
23
+ /**
24
+ * Send a transaction for signing, with a decoded + simulated preview attached so the
25
+ * wallet page can show what it actually does instead of raw calldata.
26
+ */
27
+ async function requestSignature(chain: ChainName, tx: RawTx & { data?: string }) {
28
+ const wallet = getWalletClient()
29
+ await wallet.waitForSigner()
30
+
31
+ const preview = await buildTxPreview(chain, tx, wallet.getAddress())
32
+
33
+ return {
34
+ txHash: await wallet.request({
35
+ id: generateRequestId(),
36
+ type: 'send_transaction',
37
+ chain,
38
+ data: { to: tx.to, value: tx.value ?? '0x0', ...(tx.data && { data: tx.data }) },
39
+ preview
40
+ }),
41
+ preview
42
+ }
43
+ }
44
+
45
+ function failure(error: unknown, message: string) {
46
+ return formatResponse({
47
+ success: false,
48
+ error: error instanceof Error ? error.message : String(error),
49
+ message
50
+ })
51
+ }
52
+
12
53
  export default {
13
54
  send_native_token: createTool(
14
55
  'Send Native Token',
15
- 'Send native tokens (ETH, MATIC, BNB, etc.) to an address. Opens browser wallet for approval.',
56
+ 'Send native tokens (ETH, MATIC, BNB, etc.) to an address. Simulates the transaction, then opens the browser wallet for approval.',
16
57
  z.object({
17
58
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
18
59
  to: z.string().describe('Recipient address'),
@@ -20,49 +61,30 @@ export default {
20
61
  data: z.string().optional().describe('Optional hex-encoded data to include with transaction')
21
62
  }),
22
63
  async (args) => {
23
- const walletServer = getWalletServer()
24
-
25
64
  try {
26
- // Parse amount to wei
27
- const value = '0x' + parseUnits(args.amount, 18).toString(16)
65
+ const to = requireAddress('recipient address', args.to)
66
+ const value = `0x${parseUnits(args.amount, 18).toString(16)}`
28
67
 
29
- const txRequest = {
30
- id: generateRequestId(),
31
- type: 'send_transaction' as const,
32
- chain: args.chain,
33
- data: {
34
- to: args.to,
35
- value,
36
- ...(args.data && { data: args.data })
37
- }
38
- }
39
-
40
- console.error(`[Transaction] Sending ${args.amount} native token to ${args.to} on ${args.chain}`)
41
- const txHash = await walletServer.sendTransaction(txRequest)
68
+ const { txHash, preview } = await requestSignature(args.chain as ChainName, { to, value, data: args.data })
42
69
 
43
70
  return formatResponse({
44
71
  success: true,
45
72
  chain: args.chain,
46
73
  transactionHash: txHash,
47
- to: args.to,
74
+ to,
48
75
  amount: args.amount,
49
- message: `Successfully sent ${args.amount} native token`,
50
- explorerUrl: `https://etherscan.io/tx/${txHash}`
76
+ simulation: preview.simulation,
77
+ explorerUrl: explorerTxUrl(args.chain as ChainName, txHash)
51
78
  })
52
79
  } catch (error) {
53
- const errorMessage = error instanceof Error ? error.message : String(error)
54
- return formatResponse({
55
- success: false,
56
- error: errorMessage,
57
- message: 'Transaction failed or was rejected'
58
- })
80
+ return failure(error, 'Transaction failed or was rejected')
59
81
  }
60
82
  }
61
83
  ),
62
84
 
63
85
  send_erc20_token: createTool(
64
86
  'Send ERC20 Token',
65
- 'Send ERC20 tokens to an address. Opens browser wallet for approval.',
87
+ 'Send ERC20 tokens to an address. Simulates the transfer, then opens the browser wallet for approval.',
66
88
  z.object({
67
89
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
68
90
  tokenAddress: z.string().describe('ERC20 token contract address'),
@@ -71,111 +93,71 @@ export default {
71
93
  decimals: z.number().optional().default(18).describe('Token decimals (default: 18)')
72
94
  }),
73
95
  async (args) => {
74
- const walletServer = getWalletServer()
75
-
76
96
  try {
77
- const decimals = args.decimals || 18
78
- const amountWei = parseUnits(args.amount, decimals)
79
-
80
- // ERC20 transfer(address to, uint256 amount)
81
- const data = `0xa9059cbb${args.to.slice(2).padStart(64, '0')}${amountWei.toString(16).padStart(64, '0')}`
97
+ const tokenAddress = requireAddress('token address', args.tokenAddress)
98
+ const to = requireAddress('recipient address', args.to)
82
99
 
83
- const txRequest = {
84
- id: generateRequestId(),
85
- type: 'send_transaction' as const,
86
- chain: args.chain,
87
- data: {
88
- to: args.tokenAddress,
89
- data,
90
- value: '0x0'
91
- }
92
- }
100
+ const data = encodeFunctionData({
101
+ abi: [parseAbiItem('function transfer(address to, uint256 amount)')],
102
+ args: [to, parseUnits(args.amount, args.decimals ?? 18)]
103
+ })
93
104
 
94
- console.error(`[Transaction] Sending ${args.amount} tokens to ${args.to} on ${args.chain}`)
95
- const txHash = await walletServer.sendTransaction(txRequest)
105
+ const { txHash, preview } = await requestSignature(args.chain as ChainName, { to: tokenAddress, data, value: '0x0' })
96
106
 
97
107
  return formatResponse({
98
108
  success: true,
99
109
  chain: args.chain,
100
110
  transactionHash: txHash,
101
- tokenAddress: args.tokenAddress,
102
- to: args.to,
111
+ tokenAddress,
112
+ to,
103
113
  amount: args.amount,
104
- message: `Successfully sent ${args.amount} tokens`,
105
- explorerUrl: `https://etherscan.io/tx/${txHash}`
114
+ simulation: preview.simulation,
115
+ explorerUrl: explorerTxUrl(args.chain as ChainName, txHash)
106
116
  })
107
117
  } catch (error) {
108
- const errorMessage = error instanceof Error ? error.message : String(error)
109
- return formatResponse({
110
- success: false,
111
- error: errorMessage,
112
- message: 'Token transfer failed or was rejected'
113
- })
118
+ return failure(error, 'Token transfer failed or was rejected')
114
119
  }
115
120
  }
116
121
  ),
117
122
 
118
123
  call_contract_write: createTool(
119
124
  'Call Contract (Write)',
120
- 'Call a state-changing contract function (write operation). Opens browser wallet for approval.',
125
+ 'Call a state-changing contract function. Simulates the call, then opens the browser wallet for approval.',
121
126
  z.object({
122
127
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
123
128
  contractAddress: z.string().describe('Contract address'),
124
129
  functionAbi: z.string().describe('Function ABI definition (e.g., "function transfer(address to, uint256 amount)")'),
125
- args: z.array(z.union([z.string(), z.number(), z.boolean()])).optional().describe('Function arguments in order matching the ABI signature'),
126
- value: z.string().optional().describe('Optional ETH value to send with transaction (in ETH units, e.g., "0.1")')
130
+ args: z
131
+ .array(z.union([z.string(), z.number(), z.boolean()]))
132
+ .optional()
133
+ .describe('Function arguments in order matching the ABI signature'),
134
+ value: z.string().optional().describe('Optional native value to send with transaction (in ETH units, e.g., "0.1")')
127
135
  }),
128
136
  async (args) => {
129
- const walletServer = getWalletServer()
130
-
131
137
  try {
132
- // Parse the function ABI and encode the call data
133
- console.error(`[Transaction] Parsing ABI: ${args.functionAbi}`)
134
- console.error(`[Transaction] Args: ${JSON.stringify(args.args)}`)
135
-
138
+ const contractAddress = requireAddress('contract address', args.contractAddress)
136
139
  const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
137
- console.error(`[Transaction] Parsed function: ${abiItem.name}`)
138
140
 
139
141
  const data = encodeFunctionData({
140
142
  abi: [abiItem],
141
143
  functionName: abiItem.name,
142
144
  args: (args.args || []) as readonly unknown[]
143
145
  })
144
- console.error(`[Transaction] Encoded data: ${data}`)
146
+ const value = args.value ? `0x${parseUnits(args.value, 18).toString(16)}` : '0x0'
145
147
 
146
- const valueHex = args.value ? '0x' + parseUnits(args.value, 18).toString(16) : '0x0'
147
-
148
- const txRequest = {
149
- id: generateRequestId(),
150
- type: 'send_transaction' as const,
151
- chain: args.chain,
152
- data: {
153
- to: args.contractAddress,
154
- data,
155
- value: valueHex
156
- }
157
- }
158
-
159
- console.error(`[Transaction] Calling ${abiItem.name}() on ${args.contractAddress} (${args.chain})`)
160
- const txHash = await walletServer.sendTransaction(txRequest)
148
+ const { txHash, preview } = await requestSignature(args.chain as ChainName, { to: contractAddress, data, value })
161
149
 
162
150
  return formatResponse({
163
151
  success: true,
164
152
  chain: args.chain,
165
153
  transactionHash: txHash,
166
- contractAddress: args.contractAddress,
154
+ contractAddress,
167
155
  functionName: abiItem.name,
168
- message: `Contract call to ${abiItem.name}() successful`,
169
- explorerUrl: `https://etherscan.io/tx/${txHash}`
156
+ simulation: preview.simulation,
157
+ explorerUrl: explorerTxUrl(args.chain as ChainName, txHash)
170
158
  })
171
159
  } catch (error) {
172
- console.error(`[Transaction] Error:`, error)
173
- const errorMessage = error instanceof Error ? error.message : String(error)
174
- return formatResponse({
175
- success: false,
176
- error: errorMessage,
177
- message: 'Contract call failed or was rejected'
178
- })
160
+ return failure(error, 'Contract call failed or was rejected')
179
161
  }
180
162
  }
181
163
  ),
@@ -187,20 +169,13 @@ export default {
187
169
  message: z.string().describe('Message to sign')
188
170
  }),
189
171
  async (args) => {
190
- const walletServer = getWalletServer()
191
-
192
172
  try {
193
- const request = {
173
+ const signature = await getWalletClient().request({
194
174
  id: generateRequestId(),
195
- type: 'sign_message' as const,
175
+ type: 'sign_message',
196
176
  chain: 'any',
197
- data: {
198
- message: args.message
199
- }
200
- }
201
-
202
- console.error(`[Transaction] Signing message`)
203
- const signature = await walletServer.sendTransaction(request)
177
+ data: { message: args.message }
178
+ })
204
179
 
205
180
  return formatResponse({
206
181
  success: true,
@@ -209,12 +184,7 @@ export default {
209
184
  signatureType: 'personal_sign'
210
185
  })
211
186
  } catch (error) {
212
- const errorMessage = error instanceof Error ? error.message : String(error)
213
- return formatResponse({
214
- success: false,
215
- error: errorMessage,
216
- message: 'Message signing failed or was rejected'
217
- })
187
+ return failure(error, 'Message signing failed or was rejected')
218
188
  }
219
189
  }
220
190
  ),
@@ -224,23 +194,26 @@ export default {
224
194
  'Check if a wallet is connected to the browser interface',
225
195
  z.object({}),
226
196
  async () => {
227
- const walletServer = getWalletServer()
228
- const isConnected = walletServer.isConnected()
229
- const port = walletServer.getPort()
197
+ const wallet = getWalletClient()
230
198
 
231
- // Auto-open browser if no wallet connected
232
- if (!isConnected) {
233
- walletServer.openBrowser()
199
+ try {
200
+ await wallet.connect()
201
+ } catch (error) {
202
+ return failure(error, 'Wallet relay unavailable')
234
203
  }
235
204
 
205
+ if (!wallet.isConnected()) wallet.openBrowser()
206
+
236
207
  return formatResponse({
237
- connected: isConnected,
238
- walletUrl: `http://localhost:${port}`,
239
- message: isConnected
208
+ connected: wallet.isConnected(),
209
+ address: wallet.getAddress(),
210
+ walletUrl: wallet.getUrl(),
211
+ openTab: wallet.getPageUrl(),
212
+ hosted: wallet.isRemote,
213
+ message: wallet.isConnected()
240
214
  ? 'Wallet is connected and ready to sign transactions'
241
- : `No wallet connected. Opening browser to connect... Visit http://localhost:${port} if it didn't open automatically.`
215
+ : `No wallet connected. Open ${wallet.getUrl()} and connect your wallet.`
242
216
  })
243
217
  }
244
218
  )
245
219
  }
246
-
package/src/utils.ts CHANGED
@@ -35,6 +35,101 @@ export function convertBigIntToString(obj: unknown): unknown {
35
35
  return obj
36
36
  }
37
37
 
38
+
39
+ // Summarize a call trace for compact output
40
+ export interface SummarizedCall {
41
+ type: string
42
+ from: string
43
+ to: string
44
+ selector?: string
45
+ error?: string
46
+ depth: number
47
+ }
48
+
49
+ export function summarizeTrace(trace: unknown): { hasError: boolean; errorPath: SummarizedCall[] | null; summary: string } {
50
+ // Use ref object to avoid TypeScript closure narrowing issues
51
+ const result: { errorPath: SummarizedCall[]; errorMessage: string } | null = findErrorInTrace(trace)
52
+
53
+ // Generate summary
54
+ let summary: string
55
+ if (result) {
56
+ const lastCall = result.errorPath[result.errorPath.length - 1]
57
+ summary = `REVERTED at depth ${lastCall.depth}: ${lastCall.to} (${lastCall.selector || 'unknown'}) - ${result.errorMessage}`
58
+ } else {
59
+ // For successful txs, just show top-level calls
60
+ const topCalls: string[] = []
61
+ const rootCall = trace as Record<string, unknown>
62
+ const subcalls = rootCall.calls as Record<string, unknown>[] | undefined
63
+ if (subcalls) {
64
+ for (const sub of subcalls) {
65
+ const type = sub.type as string
66
+ if (type !== 'DELEGATECALL') {
67
+ const input = sub.input as string | undefined
68
+ const selector = input && input.length >= 10 ? input.slice(0, 10) : '?'
69
+ topCalls.push(`${type} ${sub.to} (${selector})`)
70
+ }
71
+ }
72
+ }
73
+ summary = `SUCCESS - ${topCalls.length} top-level calls: ${topCalls.slice(0, 5).join(', ')}${topCalls.length > 5 ? '...' : ''}`
74
+ }
75
+
76
+ return {
77
+ hasError: result !== null,
78
+ errorPath: result?.errorPath ?? null,
79
+ summary
80
+ }
81
+ }
82
+
83
+ function findErrorInTrace(trace: unknown): { errorPath: SummarizedCall[]; errorMessage: string } | null {
84
+ if (!trace || typeof trace !== 'object') return null
85
+
86
+ function findError(call: Record<string, unknown>, path: SummarizedCall[]): { errorPath: SummarizedCall[]; errorMessage: string } | null {
87
+ const type = (call.type as string) || 'CALL'
88
+ const error = (call.error as string) || (call.revertReason as string)
89
+ const subcalls = call.calls as Record<string, unknown>[] | undefined
90
+ const input = call.input as string | undefined
91
+
92
+ // Skip DELEGATECALL for cleaner output
93
+ if (type === 'DELEGATECALL') {
94
+ if (subcalls) {
95
+ for (const subcall of subcalls) {
96
+ const result = findError(subcall, path)
97
+ if (result) return result
98
+ }
99
+ }
100
+ return null
101
+ }
102
+
103
+ const currentCall: SummarizedCall = {
104
+ type,
105
+ from: (call.from as string) || '',
106
+ to: (call.to as string) || '',
107
+ selector: input && input.length >= 10 ? input.slice(0, 10) : undefined,
108
+ error: error || undefined,
109
+ depth: path.length
110
+ }
111
+
112
+ const newPath = [...path, currentCall]
113
+
114
+ // Check subcalls first (error might be deeper)
115
+ if (subcalls) {
116
+ for (const subcall of subcalls) {
117
+ const result = findError(subcall, newPath)
118
+ if (result) return result
119
+ }
120
+ }
121
+
122
+ // If this call has an error and no subcall had one, this is the source
123
+ if (error) {
124
+ return { errorPath: newPath, errorMessage: error }
125
+ }
126
+
127
+ return null
128
+ }
129
+
130
+ return findError(trace as Record<string, unknown>, [])
131
+ }
132
+
38
133
  // Convert arguments to appropriate types based on ABI
39
134
  export function convertArgumentsToTypes(
40
135
  args: (string | number | boolean | null)[],