web3-tools-mcp 1.5.0 → 2.0.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 (109) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +346 -164
  3. package/dist/anvil.d.ts +1 -1
  4. package/dist/anvil.d.ts.map +1 -1
  5. package/dist/anvil.js +11 -9
  6. package/dist/anvil.js.map +1 -1
  7. package/dist/chain-meta.d.ts.map +1 -1
  8. package/dist/chain-meta.js +7 -3
  9. package/dist/chain-meta.js.map +1 -1
  10. package/dist/chains.d.ts +960 -0
  11. package/dist/chains.d.ts.map +1 -0
  12. package/dist/chains.js +108 -0
  13. package/dist/chains.js.map +1 -0
  14. package/dist/client.d.ts +10 -1717
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +33 -216
  17. package/dist/client.js.map +1 -1
  18. package/dist/http-server.d.ts +18 -0
  19. package/dist/http-server.d.ts.map +1 -0
  20. package/dist/http-server.js +89 -0
  21. package/dist/http-server.js.map +1 -0
  22. package/dist/index.js +77 -32
  23. package/dist/index.js.map +1 -1
  24. package/dist/kv-storage.d.ts +23 -0
  25. package/dist/kv-storage.d.ts.map +1 -0
  26. package/dist/kv-storage.js +69 -0
  27. package/dist/kv-storage.js.map +1 -0
  28. package/dist/oauth.d.ts +31 -0
  29. package/dist/oauth.d.ts.map +1 -0
  30. package/dist/oauth.js +227 -0
  31. package/dist/oauth.js.map +1 -0
  32. package/dist/preview.d.ts.map +1 -1
  33. package/dist/preview.js +14 -4
  34. package/dist/preview.js.map +1 -1
  35. package/dist/tools/advanced.d.ts +4 -4
  36. package/dist/tools/advanced.d.ts.map +1 -1
  37. package/dist/tools/advanced.js +228 -247
  38. package/dist/tools/advanced.js.map +1 -1
  39. package/dist/tools/balance.d.ts +1 -1
  40. package/dist/tools/balance.d.ts.map +1 -1
  41. package/dist/tools/balance.js +97 -103
  42. package/dist/tools/balance.js.map +1 -1
  43. package/dist/tools/contract-info.d.ts +3 -3
  44. package/dist/tools/contract-info.d.ts.map +1 -1
  45. package/dist/tools/contract-info.js +150 -161
  46. package/dist/tools/contract-info.js.map +1 -1
  47. package/dist/tools/contract.d.ts +4 -4
  48. package/dist/tools/contract.d.ts.map +1 -1
  49. package/dist/tools/contract.js +91 -110
  50. package/dist/tools/contract.js.map +1 -1
  51. package/dist/tools/ens.d.ts +6 -6
  52. package/dist/tools/ens.d.ts.map +1 -1
  53. package/dist/tools/ens.js +239 -297
  54. package/dist/tools/ens.js.map +1 -1
  55. package/dist/tools/gas.d.ts +4 -4
  56. package/dist/tools/gas.d.ts.map +1 -1
  57. package/dist/tools/gas.js +90 -138
  58. package/dist/tools/gas.js.map +1 -1
  59. package/dist/tools/index.d.ts +1 -1
  60. package/dist/tools/index.d.ts.map +1 -1
  61. package/dist/tools/logs.d.ts +1 -1
  62. package/dist/tools/logs.d.ts.map +1 -1
  63. package/dist/tools/logs.js +36 -37
  64. package/dist/tools/logs.js.map +1 -1
  65. package/dist/tools/signatures.d.ts.map +1 -1
  66. package/dist/tools/signatures.js +67 -89
  67. package/dist/tools/signatures.js.map +1 -1
  68. package/dist/tools/transactions.d.ts +48 -4
  69. package/dist/tools/transactions.d.ts.map +1 -1
  70. package/dist/tools/transactions.js +228 -44
  71. package/dist/tools/transactions.js.map +1 -1
  72. package/dist/types.d.ts +2 -1
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/utils.d.ts +3 -8
  75. package/dist/utils.d.ts.map +1 -1
  76. package/dist/utils.js +36 -98
  77. package/dist/utils.js.map +1 -1
  78. package/dist/wallet-client.d.ts +11 -1
  79. package/dist/wallet-client.d.ts.map +1 -1
  80. package/dist/wallet-client.js +27 -9
  81. package/dist/wallet-client.js.map +1 -1
  82. package/dist/walletconnect.d.ts +52 -0
  83. package/dist/walletconnect.d.ts.map +1 -0
  84. package/dist/walletconnect.js +221 -0
  85. package/dist/walletconnect.js.map +1 -0
  86. package/package.json +21 -9
  87. package/src/anvil.ts +15 -18
  88. package/src/chain-meta.ts +8 -4
  89. package/src/chains.ts +145 -0
  90. package/src/client.ts +39 -252
  91. package/src/http-server.ts +117 -0
  92. package/src/index.ts +98 -49
  93. package/src/kv-storage.ts +83 -0
  94. package/src/oauth.ts +270 -0
  95. package/src/preview.ts +15 -14
  96. package/src/tools/advanced.ts +195 -213
  97. package/src/tools/balance.ts +103 -108
  98. package/src/tools/contract-info.ts +163 -184
  99. package/src/tools/contract.ts +123 -144
  100. package/src/tools/ens.ts +290 -345
  101. package/src/tools/gas.ts +122 -170
  102. package/src/tools/index.ts +2 -2
  103. package/src/tools/logs.ts +45 -43
  104. package/src/tools/signatures.ts +68 -86
  105. package/src/tools/transactions.ts +283 -50
  106. package/src/types.ts +2 -15
  107. package/src/utils.ts +56 -127
  108. package/src/wallet-client.ts +31 -9
  109. package/src/walletconnect.ts +269 -0
@@ -1,11 +1,18 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import qrcode from 'qrcode-terminal'
3
+ import { type AbiFunction, type Address, encodeFunctionData, isAddress, parseAbiItem, parseUnits, toHex } from 'viem'
1
4
  import { z } from 'zod'
2
- import { getWalletClient } from '../wallet-client.js'
3
5
  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 { buildTxPreview, type RawTx, type TxPreview } from '../preview.js'
6
7
  import type { ChainName } from '../types.js'
7
- import { buildTxPreview, type RawTx } from '../preview.js'
8
8
  import { createTool, formatResponse } from '../utils.js'
9
+ import { getWalletClient } from '../wallet-client.js'
10
+ import { getPhoneSigner, pairingLinks } from '../walletconnect.js'
11
+
12
+ /** Render a pairing URI as an ASCII QR, so it can be scanned straight from the chat. */
13
+ function qrFor(uri: string): Promise<string> {
14
+ return new Promise((resolve) => qrcode.generate(uri, { small: true }, resolve))
15
+ }
9
16
 
10
17
  function generateRequestId(): string {
11
18
  return randomBytes(16).toString('hex')
@@ -16,15 +23,51 @@ function requireAddress(label: string, address: string): Address {
16
23
  return address
17
24
  }
18
25
 
19
- function explorerTxUrl(chain: ChainName, txHash: unknown): string {
20
- return `https://${getClientManager().getEtherscanDomain(chain)}/tx/${txHash}`
26
+ function explorerTxUrl(chain: ChainName, txHash: unknown): string | undefined {
27
+ return getClientManager().explorerUrl(chain, `/tx/${txHash}`)
21
28
  }
22
29
 
23
30
  /**
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.
31
+ * Which wallet to send the request to. Deliberately required on every signing tool: both
32
+ * signers can be live at once, and only the person holding the devices knows which one is
33
+ * actually to hand. Guessing strands the request in front of a screen nobody is looking at.
34
+ *
35
+ * Either way the transaction is decoded and simulated first — the wallet shows its own
36
+ * summary, so the preview is returned to the caller too, to be read before approving.
26
37
  */
27
- async function requestSignature(chain: ChainName, tx: RawTx & { data?: string }) {
38
+ const SignWithSchema = z
39
+ .enum(['phone', 'browser'])
40
+ .describe(
41
+ 'Where to approve this: "phone" for a paired WalletConnect wallet, "browser" for the wallet signing page. Ask the user which they want — do not assume, even when only one is connected. Call wallet_status first: it says which signers are ready, and when no signing page is open it returns walletUrl. In that case give the user that link, ask them to open it and connect a wallet, and only call this once they confirm — a signing request cannot reach a page nobody has open.'
42
+ )
43
+
44
+ type SignWith = z.infer<typeof SignWithSchema>
45
+
46
+ async function signOnPhone(chain: ChainName, tx: RawTx & { data?: string }) {
47
+ const phone = getPhoneSigner()
48
+ if (!phone) {
49
+ throw new Error(
50
+ 'Phone signing is not configured on this server. Set WALLETCONNECT_PROJECT_ID, or sign in the browser instead.'
51
+ )
52
+ }
53
+ if (!(await phone.isPaired())) {
54
+ throw new Error('No phone wallet is paired. Run pair_phone_wallet and scan the QR, or sign in the browser instead.')
55
+ }
56
+
57
+ const session = await phone.session()
58
+ const from = session?.accounts[0]
59
+ const preview = await buildTxPreview(chain, tx, from)
60
+
61
+ const txHash = await phone.request(chain, 'eth_sendTransaction', [
62
+ { from, to: tx.to, value: tx.value ?? '0x0', ...(tx.data && { data: tx.data }) }
63
+ ])
64
+
65
+ return { txHash, preview, signedWith: 'phone' as const }
66
+ }
67
+
68
+ async function requestSignature(chain: ChainName, tx: RawTx & { data?: string }, signWith: SignWith) {
69
+ if (signWith === 'phone') return signOnPhone(chain, tx)
70
+
28
71
  const wallet = getWalletClient()
29
72
  await wallet.waitForSigner()
30
73
 
@@ -38,14 +81,42 @@ async function requestSignature(chain: ChainName, tx: RawTx & { data?: string })
38
81
  data: { to: tx.to, value: tx.value ?? '0x0', ...(tx.data && { data: tx.data }) },
39
82
  preview
40
83
  }),
41
- preview
84
+ preview,
85
+ signedWith: 'browser' as const
42
86
  }
43
87
  }
44
88
 
89
+ /** What the caller should see about a transaction before it is approved. */
90
+ function previewSummary(preview: TxPreview) {
91
+ return {
92
+ action: preview.decoded?.intent ?? preview.decoded?.functionName,
93
+ protocol: preview.decoded?.protocol ?? preview.toLabel,
94
+ details: preview.decoded?.fields.map((f) => `${f.name}: ${f.value}${f.warning ? ` (${f.warning})` : ''}`),
95
+ simulation: preview.simulation
96
+ }
97
+ }
98
+
99
+ /**
100
+ * WalletConnect rejects with a plain `{ code, message }` rather than an Error, which
101
+ * String() renders as "[object Object]" — the reason a wallet gave for refusing is the
102
+ * whole point of the message, so dig it out.
103
+ */
104
+ function describeError(error: unknown): string {
105
+ if (error instanceof Error) return error.message
106
+ if (error && typeof error === 'object') {
107
+ const { message, code, reason } = error as { message?: string; code?: number; reason?: string }
108
+ const text = message ?? reason
109
+ if (text) return code === undefined ? text : `${text} (code ${code})`
110
+ return JSON.stringify(error)
111
+ }
112
+ return String(error)
113
+ }
114
+
45
115
  function failure(error: unknown, message: string) {
116
+ console.error('[Transaction]', message, '—', describeError(error))
46
117
  return formatResponse({
47
118
  success: false,
48
- error: error instanceof Error ? error.message : String(error),
119
+ error: describeError(error),
49
120
  message
50
121
  })
51
122
  }
@@ -53,19 +124,24 @@ function failure(error: unknown, message: string) {
53
124
  export default {
54
125
  send_native_token: createTool(
55
126
  'Send Native Token',
56
- 'Send native tokens (ETH, MATIC, BNB, etc.) to an address. Simulates the transaction, then opens the browser wallet for approval.',
127
+ 'Send native tokens (ETH, MATIC, BNB, etc.) to an address. Simulates the transaction, then sends it to the signer you choose for approval.',
57
128
  z.object({
58
129
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
59
130
  to: z.string().describe('Recipient address'),
60
131
  amount: z.string().describe('Amount in native token (e.g., "0.1" for 0.1 ETH)'),
61
- data: z.string().optional().describe('Optional hex-encoded data to include with transaction')
132
+ data: z.string().optional().describe('Optional hex-encoded data to include with transaction'),
133
+ signWith: SignWithSchema
62
134
  }),
63
135
  async (args) => {
64
136
  try {
65
137
  const to = requireAddress('recipient address', args.to)
66
138
  const value = `0x${parseUnits(args.amount, 18).toString(16)}`
67
139
 
68
- const { txHash, preview } = await requestSignature(args.chain as ChainName, { to, value, data: args.data })
140
+ const { txHash, preview, signedWith } = await requestSignature(
141
+ args.chain as ChainName,
142
+ { to, value, data: args.data },
143
+ args.signWith
144
+ )
69
145
 
70
146
  return formatResponse({
71
147
  success: true,
@@ -73,7 +149,8 @@ export default {
73
149
  transactionHash: txHash,
74
150
  to,
75
151
  amount: args.amount,
76
- simulation: preview.simulation,
152
+ signedWith,
153
+ preview: previewSummary(preview),
77
154
  explorerUrl: explorerTxUrl(args.chain as ChainName, txHash)
78
155
  })
79
156
  } catch (error) {
@@ -84,13 +161,14 @@ export default {
84
161
 
85
162
  send_erc20_token: createTool(
86
163
  'Send ERC20 Token',
87
- 'Send ERC20 tokens to an address. Simulates the transfer, then opens the browser wallet for approval.',
164
+ 'Send ERC20 tokens to an address. Simulates the transfer, then sends it to the signer you choose for approval.',
88
165
  z.object({
89
166
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
90
167
  tokenAddress: z.string().describe('ERC20 token contract address'),
91
168
  to: z.string().describe('Recipient address'),
92
169
  amount: z.string().describe('Amount in token units (e.g., "100" for 100 USDC)'),
93
- decimals: z.number().optional().default(18).describe('Token decimals (default: 18)')
170
+ decimals: z.number().optional().default(18).describe('Token decimals (default: 18)'),
171
+ signWith: SignWithSchema
94
172
  }),
95
173
  async (args) => {
96
174
  try {
@@ -102,7 +180,15 @@ export default {
102
180
  args: [to, parseUnits(args.amount, args.decimals ?? 18)]
103
181
  })
104
182
 
105
- const { txHash, preview } = await requestSignature(args.chain as ChainName, { to: tokenAddress, data, value: '0x0' })
183
+ const { txHash, preview, signedWith } = await requestSignature(
184
+ args.chain as ChainName,
185
+ {
186
+ to: tokenAddress,
187
+ data,
188
+ value: '0x0'
189
+ },
190
+ args.signWith
191
+ )
106
192
 
107
193
  return formatResponse({
108
194
  success: true,
@@ -111,7 +197,8 @@ export default {
111
197
  tokenAddress,
112
198
  to,
113
199
  amount: args.amount,
114
- simulation: preview.simulation,
200
+ signedWith,
201
+ preview: previewSummary(preview),
115
202
  explorerUrl: explorerTxUrl(args.chain as ChainName, txHash)
116
203
  })
117
204
  } catch (error) {
@@ -120,9 +207,9 @@ export default {
120
207
  }
121
208
  ),
122
209
 
123
- call_contract_write: createTool(
124
- 'Call Contract (Write)',
125
- 'Call a state-changing contract function. Simulates the call, then opens the browser wallet for approval.',
210
+ write_contract: createTool(
211
+ 'Write Contract',
212
+ 'Send a transaction that calls a state-changing contract function. Simulates it first, then asks your wallet to approve. Costs gas.',
126
213
  z.object({
127
214
  chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
128
215
  contractAddress: z.string().describe('Contract address'),
@@ -131,7 +218,8 @@ export default {
131
218
  .array(z.union([z.string(), z.number(), z.boolean()]))
132
219
  .optional()
133
220
  .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")')
221
+ value: z.string().optional().describe('Optional native value to send with transaction (in ETH units, e.g., "0.1")'),
222
+ signWith: SignWithSchema
135
223
  }),
136
224
  async (args) => {
137
225
  try {
@@ -145,7 +233,15 @@ export default {
145
233
  })
146
234
  const value = args.value ? `0x${parseUnits(args.value, 18).toString(16)}` : '0x0'
147
235
 
148
- const { txHash, preview } = await requestSignature(args.chain as ChainName, { to: contractAddress, data, value })
236
+ const { txHash, preview, signedWith } = await requestSignature(
237
+ args.chain as ChainName,
238
+ {
239
+ to: contractAddress,
240
+ data,
241
+ value
242
+ },
243
+ args.signWith
244
+ )
149
245
 
150
246
  return formatResponse({
151
247
  success: true,
@@ -153,7 +249,8 @@ export default {
153
249
  transactionHash: txHash,
154
250
  contractAddress,
155
251
  functionName: abiItem.name,
156
- simulation: preview.simulation,
252
+ signedWith,
253
+ preview: previewSummary(preview),
157
254
  explorerUrl: explorerTxUrl(args.chain as ChainName, txHash)
158
255
  })
159
256
  } catch (error) {
@@ -164,23 +261,37 @@ export default {
164
261
 
165
262
  sign_message: createTool(
166
263
  'Sign Message',
167
- 'Sign a message with the connected wallet. Opens browser wallet for approval.',
264
+ 'Sign a message with your wallet, on whichever signer you choose.',
168
265
  z.object({
169
- message: z.string().describe('Message to sign')
266
+ message: z.string().describe('Message to sign'),
267
+ signWith: SignWithSchema
170
268
  }),
171
269
  async (args) => {
172
270
  try {
173
- const signature = await getWalletClient().request({
174
- id: generateRequestId(),
175
- type: 'sign_message',
176
- chain: 'any',
177
- data: { message: args.message }
178
- })
271
+ let signature: unknown
272
+
273
+ if (args.signWith === 'phone') {
274
+ const phone = getPhoneSigner()
275
+ const session = await phone?.session()
276
+ if (!session) {
277
+ throw new Error('No phone wallet is paired. Run pair_phone_wallet and scan the QR, or sign in the browser instead.')
278
+ }
279
+ signature = await phone!.request('mainnet', 'personal_sign', [toHex(args.message), session.accounts[0]])
280
+ } else {
281
+ await getWalletClient().waitForSigner()
282
+ signature = await getWalletClient().request({
283
+ id: generateRequestId(),
284
+ type: 'sign_message',
285
+ chain: 'any',
286
+ data: { message: args.message }
287
+ })
288
+ }
179
289
 
180
290
  return formatResponse({
181
291
  success: true,
182
292
  message: args.message,
183
293
  signature,
294
+ signedWith: args.signWith,
184
295
  signatureType: 'personal_sign'
185
296
  })
186
297
  } catch (error) {
@@ -189,31 +300,153 @@ export default {
189
300
  }
190
301
  ),
191
302
 
303
+ pair_phone_wallet: createTool(
304
+ 'Pair Phone Wallet',
305
+ 'Pair a phone wallet over WalletConnect, so transactions are signed on the phone instead of in a browser. ' +
306
+ 'Returns a QR to scan plus links that open a wallet directly — show those to the user as clickable links. ' +
307
+ 'Requires a WalletConnect project id.',
308
+ z.object({
309
+ chains: z
310
+ .array(z.enum(SUPPORTED_CHAINS))
311
+ .optional()
312
+ .describe('Chains to request access to (defaults to every supported network)'),
313
+ wallet: z.string().optional().describe('Name of the wallet to open, e.g. "rabby" — omit to list the common ones'),
314
+ agent: z
315
+ .string()
316
+ .optional()
317
+ .describe(
318
+ 'Who is asking, shown in the wallet approval dialog, e.g. "Claude Code". Say who you actually are — ' +
319
+ 'the user reads this when deciding whether to grant signing access.'
320
+ )
321
+ }),
322
+ async (args) => {
323
+ const phone = getPhoneSigner()
324
+ if (!phone) {
325
+ return formatResponse({
326
+ success: false,
327
+ message:
328
+ 'No WalletConnect project id configured. Get a free one at https://dashboard.walletconnect.com and pass --walletconnect-project-id or set WALLETCONNECT_PROJECT_ID.'
329
+ })
330
+ }
331
+
332
+ try {
333
+ const existing = await phone.session()
334
+ if (existing) {
335
+ return formatResponse({
336
+ alreadyPaired: true,
337
+ address: existing.accounts[0],
338
+ wallet: existing.peer,
339
+ message: 'A phone wallet is already paired. Transactions will be sent to it.'
340
+ })
341
+ }
342
+
343
+ const chains = (args.chains ?? SUPPORTED_CHAINS.filter((c) => c !== 'localhost')) as ChainName[]
344
+ const uri = await phone.pair(chains, args.agent)
345
+ const projectId = getClientManager().getConfig().walletConnectProjectId as string
346
+
347
+ return formatResponse({
348
+ qr: await qrFor(uri),
349
+ uri,
350
+ // For an agent running on the phone itself there is nothing to scan, so hand over
351
+ // links that open the wallet directly. `wallet` narrows the list to one.
352
+ openOnThisDevice: await pairingLinks(projectId, uri, args.wallet),
353
+ // Approving grants an agent standing permission to request signatures, so the
354
+ // wallet dialog names the agent rather than implying a website is connecting.
355
+ shownInWallet: args.agent ? `${args.agent} via web3-tools-mcp` : 'web3-tools-mcp (AI agent)',
356
+ howToPresent:
357
+ 'Render each openOnThisDevice entry as a clickable link labelled with the wallet name, and show the ' +
358
+ 'uri as copyable text. On a phone that tap is the whole flow. Show the QR only when the user is on a ' +
359
+ 'different device from their wallet — it is unreadable inline on the phone that holds the wallet. ' +
360
+ 'If a link does nothing, some clients drop custom schemes like rabby://, so tell them to paste the uri.',
361
+ message:
362
+ 'On another device: scan the QR with your wallet. On this device: tap one of the openOnThisDevice ' +
363
+ 'links, or copy the uri and paste it into your wallet under WalletConnect — every wallet supports ' +
364
+ 'that, and it is the one that always works. Then run wallet_status to confirm.'
365
+ })
366
+ } catch (error) {
367
+ return failure(error, 'Could not start WalletConnect pairing')
368
+ }
369
+ }
370
+ ),
371
+
372
+ disconnect_phone_wallet: createTool(
373
+ 'Disconnect Phone Wallet',
374
+ 'End every WalletConnect session and drop the pairings underneath them. Use before pairing a different wallet, or to clear out attempts that were never approved.',
375
+ z.object({}),
376
+ async () => {
377
+ const phone = getPhoneSigner()
378
+ if (!phone) {
379
+ return formatResponse({ success: false, message: 'No WalletConnect project id configured — nothing to disconnect.' })
380
+ }
381
+
382
+ try {
383
+ const before = await phone.sessions()
384
+ const removed = await phone.disconnectAll()
385
+
386
+ return formatResponse({
387
+ success: true,
388
+ disconnectedSessions: removed.sessions,
389
+ droppedPairings: removed.pairings,
390
+ wallets: before.map((session) => `${session.peer ?? 'unknown wallet'} (${session.accounts[0]})`),
391
+ message:
392
+ removed.sessions || removed.pairings
393
+ ? 'Disconnected. Transactions fall back to the browser wallet until a phone is paired again.'
394
+ : 'Nothing was connected.'
395
+ })
396
+ } catch (error) {
397
+ return failure(error, 'Could not disconnect the phone wallet')
398
+ }
399
+ }
400
+ ),
401
+
192
402
  wallet_status: createTool(
193
403
  'Wallet Status',
194
- 'Check if a wallet is connected to the browser interface',
404
+ 'Report every signer and whether it is ready, so a signing tool can be given the right signWith.',
195
405
  z.object({}),
196
406
  async () => {
407
+ // Both signers are reported, never just the first one found: signWith makes this a
408
+ // choice the user owns, so the agent has to see everything available.
409
+ const phone = getPhoneSigner()
410
+ const phoneSession = await phone?.session().catch(() => undefined)
411
+
197
412
  const wallet = getWalletClient()
413
+ const relayError = await wallet
414
+ .connect()
415
+ .then(() => undefined)
416
+ .catch((error) => describeError(error))
198
417
 
199
- try {
200
- await wallet.connect()
201
- } catch (error) {
202
- return failure(error, 'Wallet relay unavailable')
418
+ const browserReady = !relayError && wallet.isConnected()
419
+ // Only raise a tab when there is no other way to sign — a status check should not
420
+ // open a window at someone who is about to use their phone.
421
+ if (!browserReady && !phoneSession && !relayError) wallet.openBrowser()
422
+
423
+ const signers = {
424
+ phone: phoneSession
425
+ ? { ready: true, address: phoneSession.accounts[0], wallet: phoneSession.peer, chains: phoneSession.chains }
426
+ : {
427
+ ready: false,
428
+ reason: phone
429
+ ? 'No phone paired. Run pair_phone_wallet and scan the QR.'
430
+ : 'Phone signing is not configured — set WALLETCONNECT_PROJECT_ID.'
431
+ },
432
+ browser: browserReady
433
+ ? { ready: true, address: wallet.getAddress(), openTab: wallet.getPageUrl() }
434
+ : {
435
+ ready: false,
436
+ reason: relayError ?? 'No signing page is open.',
437
+ walletUrl: relayError ? undefined : wallet.getUrl()
438
+ }
203
439
  }
204
440
 
205
- if (!wallet.isConnected()) wallet.openBrowser()
206
-
207
- return formatResponse({
208
- connected: wallet.isConnected(),
209
- address: wallet.getAddress(),
210
- walletUrl: wallet.getUrl(),
211
- openTab: wallet.getPageUrl(),
212
- hosted: wallet.isRemote,
213
- message: wallet.isConnected()
214
- ? 'Wallet is connected and ready to sign transactions'
215
- : `No wallet connected. Open ${wallet.getUrl()} and connect your wallet.`
216
- })
441
+ const ready = [phoneSession ? 'phone' : undefined, browserReady ? 'browser' : undefined].filter(Boolean)
442
+ const message =
443
+ ready.length === 2
444
+ ? 'Both signers are ready. Ask the user which one to use, then pass it as signWith.'
445
+ : ready.length === 1
446
+ ? `Only ${ready[0]} is ready. Confirm with the user before signing there, or set the other one up.`
447
+ : `No signer is ready. For a browser, give the user this link and ask them to open it and connect a wallet: ${signers.browser.walletUrl ?? '(relay unavailable)'}. For a phone, run pair_phone_wallet.`
448
+
449
+ return formatResponse({ ready, signers, message })
217
450
  }
218
451
  )
219
452
  }
package/src/types.ts CHANGED
@@ -14,23 +14,10 @@ export interface Config {
14
14
  infuraApiKey?: string
15
15
  customRpcUrls?: Record<string, string>
16
16
  hypersyncApiKey?: string
17
+ walletConnectProjectId?: string
17
18
  }
18
19
 
19
- // Chain names
20
- export type ChainName =
21
- | 'mainnet'
22
- | 'arbitrum'
23
- | 'avalanche'
24
- | 'base'
25
- | 'bnb'
26
- | 'gnosis'
27
- | 'sonic'
28
- | 'optimism'
29
- | 'polygon'
30
- | 'zksync'
31
- | 'linea'
32
- | 'unichain'
33
- | 'localhost'
20
+ export type { ChainName } from './chains.js'
34
21
 
35
22
  // Tool handler result type
36
23
  export interface ToolResult {