web3-tools-mcp 2.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +182 -1
  2. package/README.md +76 -47
  3. package/dist/anvil.d.ts +17 -19
  4. package/dist/anvil.d.ts.map +1 -1
  5. package/dist/anvil.js +124 -228
  6. package/dist/anvil.js.map +1 -1
  7. package/dist/chain-meta.d.ts.map +1 -1
  8. package/dist/chain-meta.js +19 -8
  9. package/dist/chain-meta.js.map +1 -1
  10. package/dist/chains.d.ts +9 -1
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +11 -2
  13. package/dist/chains.js.map +1 -1
  14. package/dist/clear-signing.d.ts.map +1 -1
  15. package/dist/clear-signing.js +17 -6
  16. package/dist/clear-signing.js.map +1 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js.map +1 -1
  19. package/dist/context.d.ts +22 -0
  20. package/dist/context.d.ts.map +1 -0
  21. package/dist/context.js +24 -0
  22. package/dist/context.js.map +1 -0
  23. package/dist/hosted.d.ts +12 -0
  24. package/dist/hosted.d.ts.map +1 -0
  25. package/dist/hosted.js +12 -0
  26. package/dist/hosted.js.map +1 -0
  27. package/dist/http-server.d.ts +13 -0
  28. package/dist/http-server.d.ts.map +1 -1
  29. package/dist/http-server.js +22 -7
  30. package/dist/http-server.js.map +1 -1
  31. package/dist/index.js +20 -12
  32. package/dist/index.js.map +1 -1
  33. package/dist/kv-storage.d.ts +2 -1
  34. package/dist/kv-storage.d.ts.map +1 -1
  35. package/dist/kv-storage.js +5 -2
  36. package/dist/kv-storage.js.map +1 -1
  37. package/dist/lib.d.ts +24 -0
  38. package/dist/lib.d.ts.map +1 -0
  39. package/dist/lib.js +23 -0
  40. package/dist/lib.js.map +1 -0
  41. package/dist/mcp-server.d.ts +4 -0
  42. package/dist/mcp-server.d.ts.map +1 -0
  43. package/dist/mcp-server.js +43 -0
  44. package/dist/mcp-server.js.map +1 -0
  45. package/dist/oauth.d.ts +57 -8
  46. package/dist/oauth.d.ts.map +1 -1
  47. package/dist/oauth.js +133 -63
  48. package/dist/oauth.js.map +1 -1
  49. package/dist/preview.d.ts +7 -0
  50. package/dist/preview.d.ts.map +1 -1
  51. package/dist/preview.js +21 -7
  52. package/dist/preview.js.map +1 -1
  53. package/dist/signing-requests.d.ts +81 -0
  54. package/dist/signing-requests.d.ts.map +1 -0
  55. package/dist/signing-requests.js +138 -0
  56. package/dist/signing-requests.js.map +1 -0
  57. package/dist/tools/index.d.ts +27 -1
  58. package/dist/tools/index.d.ts.map +1 -1
  59. package/dist/tools/index.js +50 -15
  60. package/dist/tools/index.js.map +1 -1
  61. package/dist/tools/{advanced.d.ts → read/advanced.d.ts} +39 -30
  62. package/dist/tools/read/advanced.d.ts.map +1 -0
  63. package/dist/tools/{advanced.js → read/advanced.js} +53 -41
  64. package/dist/tools/read/advanced.js.map +1 -0
  65. package/dist/tools/{balance.d.ts → read/balance.d.ts} +8 -7
  66. package/dist/tools/read/balance.d.ts.map +1 -0
  67. package/dist/tools/{balance.js → read/balance.js} +18 -33
  68. package/dist/tools/read/balance.js.map +1 -0
  69. package/dist/tools/{contract-info.d.ts → read/contract-info.d.ts} +20 -15
  70. package/dist/tools/read/contract-info.d.ts.map +1 -0
  71. package/dist/tools/{contract-info.js → read/contract-info.js} +54 -123
  72. package/dist/tools/read/contract-info.js.map +1 -0
  73. package/dist/tools/{contract.d.ts → read/contract.d.ts} +14 -12
  74. package/dist/tools/read/contract.d.ts.map +1 -0
  75. package/dist/tools/{contract.js → read/contract.js} +3 -2
  76. package/dist/tools/read/contract.js.map +1 -0
  77. package/dist/tools/read/ens.d.ts +104 -0
  78. package/dist/tools/read/ens.d.ts.map +1 -0
  79. package/dist/tools/{ens.js → read/ens.js} +25 -33
  80. package/dist/tools/read/ens.js.map +1 -0
  81. package/dist/tools/read/gas.d.ts +171 -0
  82. package/dist/tools/read/gas.d.ts.map +1 -0
  83. package/dist/tools/read/gas.js +333 -0
  84. package/dist/tools/read/gas.js.map +1 -0
  85. package/dist/tools/read/logs.d.ts +101 -0
  86. package/dist/tools/read/logs.d.ts.map +1 -0
  87. package/dist/tools/read/logs.js +211 -0
  88. package/dist/tools/read/logs.js.map +1 -0
  89. package/dist/tools/{signatures.d.ts → read/signatures.d.ts} +8 -4
  90. package/dist/tools/read/signatures.d.ts.map +1 -0
  91. package/dist/tools/{signatures.js → read/signatures.js} +4 -3
  92. package/dist/tools/read/signatures.js.map +1 -0
  93. package/dist/tools/read/signing.d.ts +27 -0
  94. package/dist/tools/read/signing.d.ts.map +1 -0
  95. package/dist/tools/read/signing.js +81 -0
  96. package/dist/tools/read/signing.js.map +1 -0
  97. package/dist/tools/sign/errors.d.ts +9 -0
  98. package/dist/tools/sign/errors.d.ts.map +1 -0
  99. package/dist/tools/sign/errors.js +29 -0
  100. package/dist/tools/sign/errors.js.map +1 -0
  101. package/dist/tools/sign/transactions.d.ts +141 -0
  102. package/dist/tools/sign/transactions.d.ts.map +1 -0
  103. package/dist/tools/sign/transactions.js +361 -0
  104. package/dist/tools/sign/transactions.js.map +1 -0
  105. package/dist/tools/sign/wallet.d.ts +50 -0
  106. package/dist/tools/sign/wallet.d.ts.map +1 -0
  107. package/dist/tools/sign/wallet.js +180 -0
  108. package/dist/tools/sign/wallet.js.map +1 -0
  109. package/dist/utils.d.ts +23 -2
  110. package/dist/utils.d.ts.map +1 -1
  111. package/dist/utils.js +48 -3
  112. package/dist/utils.js.map +1 -1
  113. package/dist/wallet-client.d.ts +34 -4
  114. package/dist/wallet-client.d.ts.map +1 -1
  115. package/dist/wallet-client.js +106 -27
  116. package/dist/wallet-client.js.map +1 -1
  117. package/dist/walletconnect.d.ts +100 -13
  118. package/dist/walletconnect.d.ts.map +1 -1
  119. package/dist/walletconnect.js +215 -50
  120. package/dist/walletconnect.js.map +1 -1
  121. package/package.json +12 -9
  122. package/src/anvil.ts +150 -271
  123. package/src/chain-meta.ts +16 -7
  124. package/src/chains.ts +14 -2
  125. package/src/clear-signing.ts +15 -9
  126. package/src/client.ts +0 -1
  127. package/src/context.ts +27 -0
  128. package/src/hosted.ts +11 -0
  129. package/src/http-server.ts +38 -8
  130. package/src/index.ts +20 -13
  131. package/src/kv-storage.ts +6 -3
  132. package/src/lib.ts +24 -0
  133. package/src/mcp-server.ts +47 -0
  134. package/src/oauth.ts +181 -69
  135. package/src/preview.ts +28 -7
  136. package/src/signing-requests.ts +198 -0
  137. package/src/tools/index.ts +78 -16
  138. package/src/tools/{advanced.ts → read/advanced.ts} +74 -43
  139. package/src/tools/{balance.ts → read/balance.ts} +20 -35
  140. package/src/tools/{contract-info.ts → read/contract-info.ts} +66 -146
  141. package/src/tools/{contract.ts → read/contract.ts} +4 -3
  142. package/src/tools/{ens.ts → read/ens.ts} +27 -34
  143. package/src/tools/read/gas.ts +443 -0
  144. package/src/tools/read/logs.ts +281 -0
  145. package/src/tools/{signatures.ts → read/signatures.ts} +5 -5
  146. package/src/tools/read/signing.ts +92 -0
  147. package/src/tools/sign/errors.ts +28 -0
  148. package/src/tools/sign/transactions.ts +479 -0
  149. package/src/tools/sign/wallet.ts +216 -0
  150. package/src/utils.ts +52 -3
  151. package/src/wallet-client.ts +107 -24
  152. package/src/walletconnect.ts +250 -48
  153. package/dist/tools/advanced.d.ts.map +0 -1
  154. package/dist/tools/advanced.js.map +0 -1
  155. package/dist/tools/balance.d.ts.map +0 -1
  156. package/dist/tools/balance.js.map +0 -1
  157. package/dist/tools/contract-info.d.ts.map +0 -1
  158. package/dist/tools/contract-info.js.map +0 -1
  159. package/dist/tools/contract.d.ts.map +0 -1
  160. package/dist/tools/contract.js.map +0 -1
  161. package/dist/tools/ens.d.ts +0 -119
  162. package/dist/tools/ens.d.ts.map +0 -1
  163. package/dist/tools/ens.js.map +0 -1
  164. package/dist/tools/gas.d.ts +0 -99
  165. package/dist/tools/gas.d.ts.map +0 -1
  166. package/dist/tools/gas.js +0 -181
  167. package/dist/tools/gas.js.map +0 -1
  168. package/dist/tools/logs.d.ts +0 -39
  169. package/dist/tools/logs.d.ts.map +0 -1
  170. package/dist/tools/logs.js +0 -230
  171. package/dist/tools/logs.js.map +0 -1
  172. package/dist/tools/signatures.d.ts.map +0 -1
  173. package/dist/tools/signatures.js.map +0 -1
  174. package/dist/tools/transactions.d.ts +0 -155
  175. package/dist/tools/transactions.d.ts.map +0 -1
  176. package/dist/tools/transactions.js +0 -365
  177. package/dist/tools/transactions.js.map +0 -1
  178. package/src/tools/gas.ts +0 -219
  179. package/src/tools/logs.ts +0 -293
  180. package/src/tools/transactions.ts +0 -456
@@ -0,0 +1,281 @@
1
+ import { HypersyncClient, type Log, LogField } from '@envio-dev/hypersync-client'
2
+ import {
3
+ type AbiEvent,
4
+ type Address,
5
+ decodeEventLog,
6
+ type Hex,
7
+ isAddress,
8
+ keccak256,
9
+ numberToHex,
10
+ padHex,
11
+ parseAbiItem,
12
+ toBytes,
13
+ toEventSignature
14
+ } from 'viem'
15
+ import { z } from 'zod'
16
+ import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
17
+ import type { ChainName } from '../../types.js'
18
+ import { convertEventArgsToTypes, createTool, formatResponse, rpcReason } from '../../utils.js'
19
+
20
+ /**
21
+ * A topic is always 32 bytes, so an address, a number or a short hex value has to be
22
+ * left-padded to fit. Passing one through unpadded is rejected outright by Hypersync.
23
+ */
24
+ function toTopic(value: unknown): Hex {
25
+ if (typeof value === 'number' || typeof value === 'bigint') {
26
+ return padHex(numberToHex(value), { size: 32 })
27
+ }
28
+ if (typeof value === 'boolean') {
29
+ return padHex(value ? '0x01' : '0x00', { size: 32 })
30
+ }
31
+ if (typeof value === 'string' && (isAddress(value) || value.startsWith('0x'))) {
32
+ return padHex(value.toLowerCase() as Hex, { size: 32 })
33
+ }
34
+ throw new Error(`Cannot filter on indexed value ${JSON.stringify(value)} — expected an address, number or hex string`)
35
+ }
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
+
93
+ async function getLogsWithHypersync(
94
+ chainName: ChainName,
95
+ abiItem: AbiEvent,
96
+ hypersyncApiKey?: string,
97
+ address?: string,
98
+ fromBlock?: string,
99
+ toBlock?: string,
100
+ eventArgs?: Record<string, unknown>
101
+ ): Promise<DecodedLog[]> {
102
+ const hypersyncUrl = getClientManager().getHypersyncUrl(chainName)
103
+ if (!hypersyncUrl) {
104
+ throw new Error(`Hypersync not supported for chain: ${chainName}`)
105
+ }
106
+
107
+ const client = HypersyncClient.new({
108
+ url: hypersyncUrl,
109
+ bearerToken: hypersyncApiKey
110
+ })
111
+
112
+ const topic0 = keccak256(toBytes(toEventSignature(abiItem)))
113
+
114
+ // Build topics array for filtering
115
+ const topics: (Hex | Hex[] | null)[] = [topic0]
116
+
117
+ // Add indexed parameter filtering if eventArgs provided
118
+ if (eventArgs) {
119
+ const indexedInputs = abiItem.inputs.filter((input) => input.indexed)
120
+
121
+ for (let i = 0; i < indexedInputs.length && i < 3; i++) {
122
+ const input = indexedInputs[i]
123
+ const argValue = input?.name ? eventArgs[input.name] : undefined
124
+
125
+ topics.push(argValue === undefined ? null : toTopics(argValue))
126
+ }
127
+ }
128
+
129
+ const latest = fromBlock && toBlock ? 0 : await client.getHeight()
130
+ const range = hypersyncRange(fromBlock, toBlock, latest)
131
+
132
+ const query = {
133
+ fieldSelection: {
134
+ log: [
135
+ LogField.Address,
136
+ LogField.Topic0,
137
+ LogField.Topic1,
138
+ LogField.Topic2,
139
+ LogField.Topic3,
140
+ LogField.Data,
141
+ LogField.BlockNumber,
142
+ LogField.BlockHash,
143
+ LogField.TransactionHash,
144
+ LogField.TransactionIndex,
145
+ LogField.LogIndex
146
+ ]
147
+ },
148
+ ...range,
149
+ logs: [
150
+ {
151
+ address: address ? [address] : undefined,
152
+ // An unfiltered position is an empty list, not a dropped entry: removing it would
153
+ // slide every later filter up a slot and match on the wrong parameter.
154
+ topics: topics.map((t) => (t === null ? [] : Array.isArray(t) ? t : [t]))
155
+ }
156
+ ]
157
+ }
158
+
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
+ }
167
+
168
+ return logs
169
+ .map((log: Log): DecodedLog | null => {
170
+ const topics = log.topics.filter((topic): topic is string => Boolean(topic)) as [Hex, ...Hex[]]
171
+ const data = (log.data ?? '0x') as Hex
172
+
173
+ try {
174
+ const { args } = decodeEventLog({ abi: [abiItem], data, topics })
175
+ return {
176
+ address: log.address as Address,
177
+ blockHash: log.blockHash as Hex,
178
+ blockNumber: log.blockNumber !== undefined ? BigInt(log.blockNumber) : null,
179
+ data,
180
+ logIndex: log.logIndex,
181
+ topics,
182
+ transactionHash: log.transactionHash as Hex,
183
+ transactionIndex: log.transactionIndex,
184
+ eventName: abiItem.name,
185
+ args: args ?? {}
186
+ }
187
+ } catch (error) {
188
+ console.warn('Failed to decode log:', error)
189
+ return null
190
+ }
191
+ })
192
+ .filter((log): log is DecodedLog => log !== null)
193
+ }
194
+
195
+ export default {
196
+ get_logs: createTool(
197
+ 'Query Contract Events',
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.`,
199
+ z.object({
200
+ chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
201
+ eventAbi: z
202
+ .string()
203
+ .describe('Event ABI definition (e.g., "event Transfer(address indexed from, address indexed to, uint256 value)")'),
204
+ address: z.string().optional().describe('Contract address (RECOMMENDED for performance)'),
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)'),
207
+ eventArgs: z
208
+ .record(z.union([z.string(), z.number(), z.boolean(), z.array(z.union([z.string(), z.number(), z.boolean()]))]))
209
+ .optional()
210
+ .describe(
211
+ 'Filter by indexed parameters: {"from": "0x123...", "to": "0x456..."}. A list matches any of its values: {"to": ["0x1...", "0x2..."]}'
212
+ )
213
+ }),
214
+ async (args) => {
215
+ const clientManager = getClientManager()
216
+ const chain = args.chain as ChainName
217
+ const abiItem = parseAbiItem(args.eventAbi) as AbiEvent
218
+
219
+ if (args.address && !isAddress(args.address)) {
220
+ throw new Error(`Invalid contract address: ${args.address}`)
221
+ }
222
+
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
229
+
230
+ const respond = (logs: DecodedLog[], dataSource: 'viem' | 'hypersync') => {
231
+ const capped = capLogs(logs)
232
+ return formatResponse({
233
+ ...capped,
234
+ eventSignature: toEventSignature(abiItem),
235
+ chain: args.chain,
236
+ fromBlock: fromBlockNum?.toString() || 'latest',
237
+ toBlock: toBlockNum?.toString() || 'latest',
238
+ count: capped.logs.length,
239
+ dataSource,
240
+ filters: {
241
+ address: args.address || null,
242
+ eventArgs: args.eventArgs || null
243
+ }
244
+ })
245
+ }
246
+
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)}`)
261
+
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
+ )
277
+ }
278
+ }
279
+ }
280
+ )
281
+ }
@@ -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,
@@ -10,8 +9,8 @@ import {
10
9
  toFunctionSignature
11
10
  } from 'viem'
12
11
  import { z } from 'zod'
13
- import type { AbiError } from '../types.js'
14
- import { createTool, formatResponse } from '../utils.js'
12
+ import type { AbiError } from '../../types.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({
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod'
2
+ import { collectSigningRequest } from '../../signing-requests.js'
3
+ import { createTool, formatResponse } from '../../utils.js'
4
+ import { failure } from '../sign/errors.js'
5
+
6
+ /**
7
+ * Collecting a signature that was not given within the tool call that asked for it.
8
+ *
9
+ * A read rather than a signing tool on purpose: the signature was requested — and charged
10
+ * for — once already, and a host metering signing should not bill an agent again for asking
11
+ * whether the human has got round to it yet.
12
+ */
13
+ export default {
14
+ check_signing_request: createTool(
15
+ 'Check Signing Request',
16
+ 'Collect the result of a signing request that was still waiting for approval. Waits a while for the user to approve, then reports back. Call this instead of asking for the signature again — repeating the original tool would put a second request in front of the wallet.',
17
+ z.object({
18
+ requestId: z.string().describe('The requestId returned when the signing tool said it was awaiting approval')
19
+ }),
20
+ async (args, identity) => {
21
+ try {
22
+ const request = await collectSigningRequest(args.requestId, identity)
23
+
24
+ const waitedFor = `${Math.round((Date.now() - request.startedAt) / 1000)}s`
25
+
26
+ if (request.state === 'pending') {
27
+ // Approved already and waiting on the chain is a different thing to report: there
28
+ // is nothing left for the user to do, and nothing to chase them about.
29
+ if (request.stage === 'mining') {
30
+ return formatResponse({
31
+ success: true,
32
+ status: 'mining',
33
+ requestId: request.id,
34
+ what: request.summary,
35
+ transactionHash: request.txHash,
36
+ waitingFor: waitedFor,
37
+ nextStep: 'Approved and broadcast. Call this again with the same requestId to get the mined result.',
38
+ message: 'Signed and sent. Waiting for it to be mined.'
39
+ })
40
+ }
41
+
42
+ return formatResponse({
43
+ success: true,
44
+ status: 'awaiting_approval',
45
+ requestId: request.id,
46
+ what: request.summary,
47
+ waitingFor: waitedFor,
48
+ nextStep:
49
+ 'Still not approved. Ask the user to check their wallet and approve or reject it, then call this again with the same requestId.',
50
+ message: 'Nothing has been signed yet.'
51
+ })
52
+ }
53
+
54
+ if (request.state === 'failed') {
55
+ // Only an explicit rejection proves nothing was signed. A timeout or a dropped
56
+ // connection can land after the wallet signed and broadcast.
57
+ const rejected = request.stage === 'approval' && /rejected|denied/i.test(request.error ?? '')
58
+ return formatResponse({
59
+ success: false,
60
+ status: rejected ? 'rejected' : 'outcome_unknown',
61
+ what: request.summary,
62
+ error: request.error,
63
+ ...(request.txHash && { transactionHash: request.txHash }),
64
+ message: rejected
65
+ ? 'The wallet rejected it. Nothing was signed.'
66
+ : 'It did not complete here, but it may still have been signed or broadcast. Do not retry until the wallet or a block explorer shows it did not go through.'
67
+ })
68
+ }
69
+
70
+ // A transaction carries how it ended on the chain; a message signature is just the
71
+ // signature, with nothing to mine.
72
+ const settled = request.result as { receipt?: { status: string; blockNumber?: string; gasUsed?: string } } | undefined
73
+ const reverted = settled?.receipt?.status === 'reverted'
74
+
75
+ return formatResponse({
76
+ success: !reverted,
77
+ status: settled?.receipt?.status ?? 'signed',
78
+ what: request.summary,
79
+ signedWith: request.signer,
80
+ result: request.result,
81
+ message: reverted
82
+ ? 'Mined, but it reverted. The gas was spent and nothing else changed.'
83
+ : settled?.receipt?.status === 'broadcast'
84
+ ? 'Approved and broadcast, but not mined yet.'
85
+ : 'Approved and done.'
86
+ })
87
+ } catch (error) {
88
+ return failure(error, 'Could not check that signing request')
89
+ }
90
+ }
91
+ )
92
+ }
@@ -0,0 +1,28 @@
1
+ import { log } from '../../log.js'
2
+ import { formatResponse } from '../../utils.js'
3
+
4
+ /**
5
+ * WalletConnect rejects with a plain `{ code, message }` rather than an Error, which
6
+ * String() renders as "[object Object]" — the reason a wallet gave for refusing is the
7
+ * whole point of the message, so dig it out.
8
+ */
9
+ export function describeError(error: unknown): string {
10
+ if (error instanceof Error) return error.message
11
+ if (error && typeof error === 'object') {
12
+ const { message, code, reason } = error as { message?: string; code?: number; reason?: string }
13
+ const text = message ?? reason
14
+ if (text) return code === undefined ? text : `${text} (code ${code})`
15
+ return JSON.stringify(error)
16
+ }
17
+ return String(error)
18
+ }
19
+
20
+ /** A refused or failed signing attempt, reported to the caller and to the log alike. */
21
+ export function failure(error: unknown, message: string) {
22
+ log('warning', 'Transaction', `${message} — ${describeError(error)}`)
23
+ return formatResponse({
24
+ success: false,
25
+ error: describeError(error),
26
+ message
27
+ })
28
+ }