web3-tools-mcp 1.6.0 → 2.0.1
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.
- package/CHANGELOG.md +109 -0
- package/README.md +328 -234
- package/dist/anvil.d.ts +1 -1
- package/dist/anvil.d.ts.map +1 -1
- package/dist/anvil.js +11 -9
- package/dist/anvil.js.map +1 -1
- package/dist/chain-meta.d.ts.map +1 -1
- package/dist/chain-meta.js +7 -3
- package/dist/chain-meta.js.map +1 -1
- package/dist/chains.d.ts +960 -0
- package/dist/chains.d.ts.map +1 -0
- package/dist/chains.js +108 -0
- package/dist/chains.js.map +1 -0
- package/dist/client.d.ts +10 -1717
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +33 -218
- package/dist/client.js.map +1 -1
- package/dist/http-server.d.ts +6 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +55 -40
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +50 -29
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +31 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +227 -0
- package/dist/oauth.js.map +1 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +2 -2
- package/dist/preview.js.map +1 -1
- package/dist/tools/advanced.d.ts +4 -4
- package/dist/tools/advanced.d.ts.map +1 -1
- package/dist/tools/advanced.js +228 -247
- package/dist/tools/advanced.js.map +1 -1
- package/dist/tools/balance.d.ts +1 -1
- package/dist/tools/balance.d.ts.map +1 -1
- package/dist/tools/balance.js +97 -103
- package/dist/tools/balance.js.map +1 -1
- package/dist/tools/contract-info.d.ts +3 -3
- package/dist/tools/contract-info.d.ts.map +1 -1
- package/dist/tools/contract-info.js +150 -161
- package/dist/tools/contract-info.js.map +1 -1
- package/dist/tools/contract.d.ts +4 -4
- package/dist/tools/contract.d.ts.map +1 -1
- package/dist/tools/contract.js +91 -110
- package/dist/tools/contract.js.map +1 -1
- package/dist/tools/ens.d.ts +6 -6
- package/dist/tools/ens.d.ts.map +1 -1
- package/dist/tools/ens.js +239 -297
- package/dist/tools/ens.js.map +1 -1
- package/dist/tools/gas.d.ts +4 -4
- package/dist/tools/gas.d.ts.map +1 -1
- package/dist/tools/gas.js +90 -138
- package/dist/tools/gas.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/logs.d.ts +1 -1
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +36 -37
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/signatures.d.ts.map +1 -1
- package/dist/tools/signatures.js +67 -89
- package/dist/tools/signatures.js.map +1 -1
- package/dist/tools/transactions.d.ts +35 -5
- package/dist/tools/transactions.d.ts.map +1 -1
- package/dist/tools/transactions.js +148 -74
- package/dist/tools/transactions.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +35 -98
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +11 -1
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +27 -9
- package/dist/wallet-client.js.map +1 -1
- package/dist/walletconnect.d.ts +22 -1
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js +106 -8
- package/dist/walletconnect.js.map +1 -1
- package/package.json +17 -9
- package/src/anvil.ts +15 -18
- package/src/chain-meta.ts +8 -4
- package/src/chains.ts +145 -0
- package/src/client.ts +39 -254
- package/src/http-server.ts +67 -44
- package/src/index.ts +77 -53
- package/src/oauth.ts +270 -0
- package/src/preview.ts +3 -12
- package/src/tools/advanced.ts +195 -213
- package/src/tools/balance.ts +103 -108
- package/src/tools/contract-info.ts +163 -184
- package/src/tools/contract.ts +123 -144
- package/src/tools/ens.ts +290 -345
- package/src/tools/gas.ts +122 -170
- package/src/tools/index.ts +2 -2
- package/src/tools/logs.ts +45 -43
- package/src/tools/signatures.ts +68 -86
- package/src/tools/transactions.ts +194 -87
- package/src/types.ts +1 -15
- package/src/utils.ts +52 -110
- package/src/wallet-client.ts +31 -9
- package/src/walletconnect.ts +124 -7
package/src/tools/signatures.ts
CHANGED
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
type AbiEvent,
|
|
3
3
|
type AbiFunction,
|
|
4
4
|
type AbiParameter,
|
|
5
|
+
encodeFunctionData,
|
|
5
6
|
keccak256,
|
|
6
7
|
parseAbiItem,
|
|
7
8
|
toBytes,
|
|
8
9
|
toEventSignature,
|
|
9
|
-
toFunctionSignature
|
|
10
|
-
encodeFunctionData
|
|
10
|
+
toFunctionSignature
|
|
11
11
|
} from 'viem'
|
|
12
12
|
import { z } from 'zod'
|
|
13
13
|
import type { AbiError } from '../types.js'
|
|
@@ -24,9 +24,7 @@ const EventAbiSchema = z.object({
|
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
const ErrorAbiSchema = z.object({
|
|
27
|
-
errorAbi: z
|
|
28
|
-
.string()
|
|
29
|
-
.describe('Error ABI definition (e.g., "error InsufficientBalance(uint256 available, uint256 required)")')
|
|
27
|
+
errorAbi: z.string().describe('Error ABI definition (e.g., "error InsufficientBalance(uint256 available, uint256 required)")')
|
|
30
28
|
})
|
|
31
29
|
|
|
32
30
|
export default {
|
|
@@ -34,33 +32,27 @@ export default {
|
|
|
34
32
|
'Calculate Function Signatures',
|
|
35
33
|
'Generate 4-byte function selectors from ABI definitions. BATCH OPTIMIZED - use for transaction decoding.',
|
|
36
34
|
z.object({
|
|
37
|
-
items: z
|
|
38
|
-
.array(FunctionAbiSchema)
|
|
39
|
-
.describe('Array of function ABI definitions. Batch multiple functions for efficiency.')
|
|
35
|
+
items: z.array(FunctionAbiSchema).describe('Array of function ABI definitions. Batch multiple functions for efficiency.')
|
|
40
36
|
}),
|
|
41
37
|
async (args) => {
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const selector = hash.slice(0, 10)
|
|
38
|
+
const results = args.items.map((item) => {
|
|
39
|
+
const abiItem = parseAbiItem(item.functionAbi) as AbiFunction
|
|
40
|
+
const signature = toFunctionSignature(abiItem)
|
|
41
|
+
const hash = keccak256(toBytes(signature))
|
|
42
|
+
const selector = hash.slice(0, 10)
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
return {
|
|
45
|
+
signature: selector,
|
|
46
|
+
fullSignature: signature,
|
|
47
|
+
functionName: abiItem.name,
|
|
48
|
+
inputs: abiItem.inputs,
|
|
49
|
+
outputs: abiItem.outputs || [],
|
|
50
|
+
stateMutability: abiItem.stateMutability,
|
|
51
|
+
originalAbi: item.functionAbi
|
|
52
|
+
}
|
|
53
|
+
})
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
} catch (error) {
|
|
62
|
-
throw new Error(`Failed to parse function ABI: ${error}`)
|
|
63
|
-
}
|
|
55
|
+
return formatResponse(results)
|
|
64
56
|
}
|
|
65
57
|
),
|
|
66
58
|
|
|
@@ -71,27 +63,23 @@ export default {
|
|
|
71
63
|
items: z.array(EventAbiSchema).describe('Array of event ABI definitions. Batch multiple events for optimal performance.')
|
|
72
64
|
}),
|
|
73
65
|
async (args) => {
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const hash = keccak256(toBytes(signature))
|
|
66
|
+
const results = args.items.map((item) => {
|
|
67
|
+
const abiItem = parseAbiItem(item.eventAbi) as AbiEvent
|
|
68
|
+
const signature = toEventSignature(abiItem)
|
|
69
|
+
const hash = keccak256(toBytes(signature))
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
71
|
+
return {
|
|
72
|
+
signature: hash,
|
|
73
|
+
topic0: hash,
|
|
74
|
+
fullSignature: signature,
|
|
75
|
+
eventName: abiItem.name,
|
|
76
|
+
inputs: abiItem.inputs,
|
|
77
|
+
anonymous: abiItem.anonymous || false,
|
|
78
|
+
originalAbi: item.eventAbi
|
|
79
|
+
}
|
|
80
|
+
})
|
|
90
81
|
|
|
91
|
-
|
|
92
|
-
} catch (error) {
|
|
93
|
-
throw new Error(`Failed to parse event ABI: ${error}`)
|
|
94
|
-
}
|
|
82
|
+
return formatResponse(results)
|
|
95
83
|
}
|
|
96
84
|
),
|
|
97
85
|
|
|
@@ -102,58 +90,52 @@ export default {
|
|
|
102
90
|
items: z.array(ErrorAbiSchema).describe('Array of error ABI definitions. Batch related errors for efficiency.')
|
|
103
91
|
}),
|
|
104
92
|
async (args) => {
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const selector = hash.slice(0, 10)
|
|
93
|
+
const results = args.items.map((item) => {
|
|
94
|
+
const abiItem = parseAbiItem(item.errorAbi) as AbiError
|
|
95
|
+
const signature = `${abiItem.name}(${abiItem.inputs.map((input: AbiParameter) => input.type).join(',')})`
|
|
96
|
+
const hash = keccak256(toBytes(signature))
|
|
97
|
+
const selector = hash.slice(0, 10)
|
|
111
98
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
return {
|
|
100
|
+
signature: selector,
|
|
101
|
+
fullSignature: signature,
|
|
102
|
+
errorName: abiItem.name,
|
|
103
|
+
inputs: abiItem.inputs,
|
|
104
|
+
originalAbi: item.errorAbi
|
|
105
|
+
}
|
|
106
|
+
})
|
|
120
107
|
|
|
121
|
-
|
|
122
|
-
} catch (error) {
|
|
123
|
-
throw new Error(`Failed to parse error ABI: ${error}`)
|
|
124
|
-
}
|
|
108
|
+
return formatResponse(results)
|
|
125
109
|
}
|
|
126
110
|
),
|
|
127
111
|
|
|
128
112
|
encode_function_data: createTool(
|
|
129
113
|
'Encode Function Call Data',
|
|
130
|
-
'Encode a function call with parameters into transaction data. Use this before calling
|
|
114
|
+
'Encode a function call with parameters into transaction data. Use this before calling write_contract.',
|
|
131
115
|
z.object({
|
|
132
116
|
functionAbi: z.string().describe('Function ABI definition (e.g., "function transfer(address to, uint256 amount)")'),
|
|
133
|
-
args: z
|
|
117
|
+
args: z
|
|
118
|
+
.array(z.union([z.string(), z.number(), z.boolean()]))
|
|
119
|
+
.describe('Function arguments in order matching the ABI signature. Automatically type-converted.')
|
|
134
120
|
}),
|
|
135
121
|
async (args) => {
|
|
136
|
-
|
|
137
|
-
const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
|
|
122
|
+
const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
124
|
+
// Encode the function data
|
|
125
|
+
const data = encodeFunctionData({
|
|
126
|
+
abi: [abiItem],
|
|
127
|
+
functionName: abiItem.name,
|
|
128
|
+
args: args.args as readonly unknown[]
|
|
129
|
+
})
|
|
145
130
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch (error) {
|
|
155
|
-
throw new Error(`Failed to encode function data: ${error}`)
|
|
156
|
-
}
|
|
131
|
+
return formatResponse({
|
|
132
|
+
data,
|
|
133
|
+
functionName: abiItem.name,
|
|
134
|
+
functionSignature: toFunctionSignature(abiItem),
|
|
135
|
+
selector: data.slice(0, 10),
|
|
136
|
+
encodedArgs: data.slice(10),
|
|
137
|
+
message: 'Function data encoded successfully. Use this data with write_contract tool.'
|
|
138
|
+
})
|
|
157
139
|
}
|
|
158
140
|
)
|
|
159
141
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
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, toHex, type AbiFunction, type Address } from 'viem'
|
|
5
|
-
import { randomBytes } from 'node:crypto'
|
|
6
|
-
import type { ChainName } from '../types.js'
|
|
7
6
|
import { buildTxPreview, type RawTx, type TxPreview } from '../preview.js'
|
|
8
|
-
import {
|
|
7
|
+
import type { ChainName } from '../types.js'
|
|
9
8
|
import { createTool, formatResponse } from '../utils.js'
|
|
10
|
-
import
|
|
9
|
+
import { getWalletClient } from '../wallet-client.js'
|
|
10
|
+
import { getPhoneSigner, pairingLinks } from '../walletconnect.js'
|
|
11
11
|
|
|
12
12
|
/** Render a pairing URI as an ASCII QR, so it can be scanned straight from the chat. */
|
|
13
13
|
function qrFor(uri: string): Promise<string> {
|
|
@@ -23,32 +23,50 @@ function requireAddress(label: string, address: string): Address {
|
|
|
23
23
|
return address
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function explorerTxUrl(chain: ChainName, txHash: unknown): string {
|
|
27
|
-
return
|
|
26
|
+
function explorerTxUrl(chain: ChainName, txHash: unknown): string | undefined {
|
|
27
|
+
return getClientManager().explorerUrl(chain, `/tx/${txHash}`)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
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.
|
|
32
34
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* simulated first — the phone wallet shows its own summary, so the preview is returned to
|
|
36
|
-
* the caller as well, where it can be read before the prompt is approved.
|
|
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.
|
|
37
37
|
*/
|
|
38
|
-
|
|
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 }) {
|
|
39
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
|
+
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const preview = await buildTxPreview(chain, tx, from)
|
|
57
|
+
const session = await phone.session()
|
|
58
|
+
const from = session?.accounts[0]
|
|
59
|
+
const preview = await buildTxPreview(chain, tx, from)
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
const txHash = await phone.request(chain, 'eth_sendTransaction', [
|
|
62
|
+
{ from, to: tx.to, value: tx.value ?? '0x0', ...(tx.data && { data: tx.data }) }
|
|
63
|
+
])
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
52
70
|
|
|
53
71
|
const wallet = getWalletClient()
|
|
54
72
|
await wallet.waitForSigner()
|
|
@@ -106,19 +124,24 @@ function failure(error: unknown, message: string) {
|
|
|
106
124
|
export default {
|
|
107
125
|
send_native_token: createTool(
|
|
108
126
|
'Send Native Token',
|
|
109
|
-
'Send native tokens (ETH, MATIC, BNB, etc.) to an address. Simulates the transaction, then
|
|
127
|
+
'Send native tokens (ETH, MATIC, BNB, etc.) to an address. Simulates the transaction, then sends it to the signer you choose for approval.',
|
|
110
128
|
z.object({
|
|
111
129
|
chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
|
|
112
130
|
to: z.string().describe('Recipient address'),
|
|
113
131
|
amount: z.string().describe('Amount in native token (e.g., "0.1" for 0.1 ETH)'),
|
|
114
|
-
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
|
|
115
134
|
}),
|
|
116
135
|
async (args) => {
|
|
117
136
|
try {
|
|
118
137
|
const to = requireAddress('recipient address', args.to)
|
|
119
138
|
const value = `0x${parseUnits(args.amount, 18).toString(16)}`
|
|
120
139
|
|
|
121
|
-
const { txHash, preview, signedWith } = await requestSignature(
|
|
140
|
+
const { txHash, preview, signedWith } = await requestSignature(
|
|
141
|
+
args.chain as ChainName,
|
|
142
|
+
{ to, value, data: args.data },
|
|
143
|
+
args.signWith
|
|
144
|
+
)
|
|
122
145
|
|
|
123
146
|
return formatResponse({
|
|
124
147
|
success: true,
|
|
@@ -138,13 +161,14 @@ export default {
|
|
|
138
161
|
|
|
139
162
|
send_erc20_token: createTool(
|
|
140
163
|
'Send ERC20 Token',
|
|
141
|
-
'Send ERC20 tokens to an address. Simulates the transfer, then
|
|
164
|
+
'Send ERC20 tokens to an address. Simulates the transfer, then sends it to the signer you choose for approval.',
|
|
142
165
|
z.object({
|
|
143
166
|
chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
|
|
144
167
|
tokenAddress: z.string().describe('ERC20 token contract address'),
|
|
145
168
|
to: z.string().describe('Recipient address'),
|
|
146
169
|
amount: z.string().describe('Amount in token units (e.g., "100" for 100 USDC)'),
|
|
147
|
-
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
|
|
148
172
|
}),
|
|
149
173
|
async (args) => {
|
|
150
174
|
try {
|
|
@@ -156,7 +180,15 @@ export default {
|
|
|
156
180
|
args: [to, parseUnits(args.amount, args.decimals ?? 18)]
|
|
157
181
|
})
|
|
158
182
|
|
|
159
|
-
const { txHash, preview, signedWith } = await requestSignature(
|
|
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
|
+
)
|
|
160
192
|
|
|
161
193
|
return formatResponse({
|
|
162
194
|
success: true,
|
|
@@ -175,9 +207,9 @@ export default {
|
|
|
175
207
|
}
|
|
176
208
|
),
|
|
177
209
|
|
|
178
|
-
|
|
179
|
-
'
|
|
180
|
-
'
|
|
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.',
|
|
181
213
|
z.object({
|
|
182
214
|
chain: z.enum(SUPPORTED_CHAINS).describe('Blockchain network'),
|
|
183
215
|
contractAddress: z.string().describe('Contract address'),
|
|
@@ -186,7 +218,8 @@ export default {
|
|
|
186
218
|
.array(z.union([z.string(), z.number(), z.boolean()]))
|
|
187
219
|
.optional()
|
|
188
220
|
.describe('Function arguments in order matching the ABI signature'),
|
|
189
|
-
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
|
|
190
223
|
}),
|
|
191
224
|
async (args) => {
|
|
192
225
|
try {
|
|
@@ -200,7 +233,15 @@ export default {
|
|
|
200
233
|
})
|
|
201
234
|
const value = args.value ? `0x${parseUnits(args.value, 18).toString(16)}` : '0x0'
|
|
202
235
|
|
|
203
|
-
const { txHash, preview, signedWith } = await requestSignature(
|
|
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
|
+
)
|
|
204
245
|
|
|
205
246
|
return formatResponse({
|
|
206
247
|
success: true,
|
|
@@ -220,30 +261,37 @@ export default {
|
|
|
220
261
|
|
|
221
262
|
sign_message: createTool(
|
|
222
263
|
'Sign Message',
|
|
223
|
-
'Sign a message with
|
|
264
|
+
'Sign a message with your wallet, on whichever signer you choose.',
|
|
224
265
|
z.object({
|
|
225
|
-
message: z.string().describe('Message to sign')
|
|
266
|
+
message: z.string().describe('Message to sign'),
|
|
267
|
+
signWith: SignWithSchema
|
|
226
268
|
}),
|
|
227
269
|
async (args) => {
|
|
228
270
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
289
|
|
|
242
290
|
return formatResponse({
|
|
243
291
|
success: true,
|
|
244
292
|
message: args.message,
|
|
245
293
|
signature,
|
|
246
|
-
signedWith:
|
|
294
|
+
signedWith: args.signWith,
|
|
247
295
|
signatureType: 'personal_sign'
|
|
248
296
|
})
|
|
249
297
|
} catch (error) {
|
|
@@ -254,12 +302,22 @@ export default {
|
|
|
254
302
|
|
|
255
303
|
pair_phone_wallet: createTool(
|
|
256
304
|
'Pair Phone Wallet',
|
|
257
|
-
'Pair a phone wallet over WalletConnect, so transactions are signed on the phone instead of in a browser.
|
|
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.',
|
|
258
308
|
z.object({
|
|
259
309
|
chains: z
|
|
260
310
|
.array(z.enum(SUPPORTED_CHAINS))
|
|
261
311
|
.optional()
|
|
262
|
-
.describe('Chains to request access to (defaults to every supported network)')
|
|
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
|
+
)
|
|
263
321
|
}),
|
|
264
322
|
async (args) => {
|
|
265
323
|
const phone = getPhoneSigner()
|
|
@@ -267,7 +325,7 @@ export default {
|
|
|
267
325
|
return formatResponse({
|
|
268
326
|
success: false,
|
|
269
327
|
message:
|
|
270
|
-
'No WalletConnect project id configured. Get a free one at https://dashboard.
|
|
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.'
|
|
271
329
|
})
|
|
272
330
|
}
|
|
273
331
|
|
|
@@ -283,14 +341,27 @@ export default {
|
|
|
283
341
|
}
|
|
284
342
|
|
|
285
343
|
const chains = (args.chains ?? SUPPORTED_CHAINS.filter((c) => c !== 'localhost')) as ChainName[]
|
|
286
|
-
const uri = await phone.pair(chains)
|
|
344
|
+
const uri = await phone.pair(chains, args.agent)
|
|
345
|
+
const projectId = getClientManager().getConfig().walletConnectProjectId as string
|
|
287
346
|
|
|
288
347
|
return formatResponse({
|
|
289
348
|
qr: await qrFor(uri),
|
|
290
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.',
|
|
291
361
|
message:
|
|
292
|
-
'
|
|
293
|
-
'
|
|
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.'
|
|
294
365
|
})
|
|
295
366
|
} catch (error) {
|
|
296
367
|
return failure(error, 'Could not start WalletConnect pairing')
|
|
@@ -298,48 +369,84 @@ export default {
|
|
|
298
369
|
}
|
|
299
370
|
),
|
|
300
371
|
|
|
301
|
-
|
|
302
|
-
'Wallet
|
|
303
|
-
'
|
|
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.',
|
|
304
375
|
z.object({}),
|
|
305
376
|
async () => {
|
|
306
|
-
// A paired phone is the signer of record, so look there first. Connecting to the
|
|
307
|
-
// browser relay before checking would start one on a hosted server, which has no
|
|
308
|
-
// browser to open it in.
|
|
309
377
|
const phone = getPhoneSigner()
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
|
|
312
386
|
return formatResponse({
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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.'
|
|
319
395
|
})
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return failure(error, 'Could not disconnect the phone wallet')
|
|
320
398
|
}
|
|
399
|
+
}
|
|
400
|
+
),
|
|
401
|
+
|
|
402
|
+
wallet_status: createTool(
|
|
403
|
+
'Wallet Status',
|
|
404
|
+
'Report every signer and whether it is ready, so a signing tool can be given the right signWith.',
|
|
405
|
+
z.object({}),
|
|
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)
|
|
321
411
|
|
|
322
412
|
const wallet = getWalletClient()
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
413
|
+
const relayError = await wallet
|
|
414
|
+
.connect()
|
|
415
|
+
.then(() => undefined)
|
|
416
|
+
.catch((error) => describeError(error))
|
|
417
|
+
|
|
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
|
+
}
|
|
327
439
|
}
|
|
328
440
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
hosted: wallet.isRemote,
|
|
339
|
-
message: wallet.isConnected()
|
|
340
|
-
? 'Wallet is connected and ready to sign transactions'
|
|
341
|
-
: `No wallet connected. Open ${wallet.getUrl()} and connect your wallet.`
|
|
342
|
-
})
|
|
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 })
|
|
343
450
|
}
|
|
344
451
|
)
|
|
345
452
|
}
|
package/src/types.ts
CHANGED
|
@@ -17,21 +17,7 @@ export interface Config {
|
|
|
17
17
|
walletConnectProjectId?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
export type ChainName =
|
|
22
|
-
| 'mainnet'
|
|
23
|
-
| 'arbitrum'
|
|
24
|
-
| 'avalanche'
|
|
25
|
-
| 'base'
|
|
26
|
-
| 'bnb'
|
|
27
|
-
| 'gnosis'
|
|
28
|
-
| 'sonic'
|
|
29
|
-
| 'optimism'
|
|
30
|
-
| 'polygon'
|
|
31
|
-
| 'zksync'
|
|
32
|
-
| 'linea'
|
|
33
|
-
| 'unichain'
|
|
34
|
-
| 'localhost'
|
|
20
|
+
export type { ChainName } from './chains.js'
|
|
35
21
|
|
|
36
22
|
// Tool handler result type
|
|
37
23
|
export interface ToolResult {
|