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.
- package/CHANGELOG.md +182 -1
- package/README.md +76 -47
- package/dist/anvil.d.ts +17 -19
- package/dist/anvil.d.ts.map +1 -1
- package/dist/anvil.js +124 -228
- package/dist/anvil.js.map +1 -1
- package/dist/chain-meta.d.ts.map +1 -1
- package/dist/chain-meta.js +19 -8
- package/dist/chain-meta.js.map +1 -1
- package/dist/chains.d.ts +9 -1
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -2
- package/dist/chains.js.map +1 -1
- package/dist/clear-signing.d.ts.map +1 -1
- package/dist/clear-signing.js +17 -6
- package/dist/clear-signing.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +24 -0
- package/dist/context.js.map +1 -0
- package/dist/hosted.d.ts +12 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/hosted.js +12 -0
- package/dist/hosted.js.map +1 -0
- package/dist/http-server.d.ts +13 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +22 -7
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +20 -12
- package/dist/index.js.map +1 -1
- package/dist/kv-storage.d.ts +2 -1
- package/dist/kv-storage.d.ts.map +1 -1
- package/dist/kv-storage.js +5 -2
- package/dist/kv-storage.js.map +1 -1
- package/dist/lib.d.ts +24 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +23 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +43 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/oauth.d.ts +57 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +133 -63
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts +7 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +21 -7
- package/dist/preview.js.map +1 -1
- package/dist/signing-requests.d.ts +81 -0
- package/dist/signing-requests.d.ts.map +1 -0
- package/dist/signing-requests.js +138 -0
- package/dist/signing-requests.js.map +1 -0
- package/dist/tools/index.d.ts +27 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +50 -15
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/{advanced.d.ts → read/advanced.d.ts} +39 -30
- package/dist/tools/read/advanced.d.ts.map +1 -0
- package/dist/tools/{advanced.js → read/advanced.js} +53 -41
- package/dist/tools/read/advanced.js.map +1 -0
- package/dist/tools/{balance.d.ts → read/balance.d.ts} +8 -7
- package/dist/tools/read/balance.d.ts.map +1 -0
- package/dist/tools/{balance.js → read/balance.js} +18 -33
- package/dist/tools/read/balance.js.map +1 -0
- package/dist/tools/{contract-info.d.ts → read/contract-info.d.ts} +20 -15
- package/dist/tools/read/contract-info.d.ts.map +1 -0
- package/dist/tools/{contract-info.js → read/contract-info.js} +54 -123
- package/dist/tools/read/contract-info.js.map +1 -0
- package/dist/tools/{contract.d.ts → read/contract.d.ts} +14 -12
- package/dist/tools/read/contract.d.ts.map +1 -0
- package/dist/tools/{contract.js → read/contract.js} +3 -2
- package/dist/tools/read/contract.js.map +1 -0
- package/dist/tools/read/ens.d.ts +104 -0
- package/dist/tools/read/ens.d.ts.map +1 -0
- package/dist/tools/{ens.js → read/ens.js} +25 -33
- package/dist/tools/read/ens.js.map +1 -0
- package/dist/tools/read/gas.d.ts +171 -0
- package/dist/tools/read/gas.d.ts.map +1 -0
- package/dist/tools/read/gas.js +333 -0
- package/dist/tools/read/gas.js.map +1 -0
- package/dist/tools/read/logs.d.ts +101 -0
- package/dist/tools/read/logs.d.ts.map +1 -0
- package/dist/tools/read/logs.js +211 -0
- package/dist/tools/read/logs.js.map +1 -0
- package/dist/tools/{signatures.d.ts → read/signatures.d.ts} +8 -4
- package/dist/tools/read/signatures.d.ts.map +1 -0
- package/dist/tools/{signatures.js → read/signatures.js} +4 -3
- package/dist/tools/read/signatures.js.map +1 -0
- package/dist/tools/read/signing.d.ts +27 -0
- package/dist/tools/read/signing.d.ts.map +1 -0
- package/dist/tools/read/signing.js +81 -0
- package/dist/tools/read/signing.js.map +1 -0
- package/dist/tools/sign/errors.d.ts +9 -0
- package/dist/tools/sign/errors.d.ts.map +1 -0
- package/dist/tools/sign/errors.js +29 -0
- package/dist/tools/sign/errors.js.map +1 -0
- package/dist/tools/sign/transactions.d.ts +141 -0
- package/dist/tools/sign/transactions.d.ts.map +1 -0
- package/dist/tools/sign/transactions.js +361 -0
- package/dist/tools/sign/transactions.js.map +1 -0
- package/dist/tools/sign/wallet.d.ts +50 -0
- package/dist/tools/sign/wallet.d.ts.map +1 -0
- package/dist/tools/sign/wallet.js +180 -0
- package/dist/tools/sign/wallet.js.map +1 -0
- package/dist/utils.d.ts +23 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +48 -3
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +34 -4
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +106 -27
- package/dist/wallet-client.js.map +1 -1
- package/dist/walletconnect.d.ts +100 -13
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js +215 -50
- package/dist/walletconnect.js.map +1 -1
- package/package.json +12 -9
- package/src/anvil.ts +150 -271
- package/src/chain-meta.ts +16 -7
- package/src/chains.ts +14 -2
- package/src/clear-signing.ts +15 -9
- package/src/client.ts +0 -1
- package/src/context.ts +27 -0
- package/src/hosted.ts +11 -0
- package/src/http-server.ts +38 -8
- package/src/index.ts +20 -13
- package/src/kv-storage.ts +6 -3
- package/src/lib.ts +24 -0
- package/src/mcp-server.ts +47 -0
- package/src/oauth.ts +181 -69
- package/src/preview.ts +28 -7
- package/src/signing-requests.ts +198 -0
- package/src/tools/index.ts +78 -16
- package/src/tools/{advanced.ts → read/advanced.ts} +74 -43
- package/src/tools/{balance.ts → read/balance.ts} +20 -35
- package/src/tools/{contract-info.ts → read/contract-info.ts} +66 -146
- package/src/tools/{contract.ts → read/contract.ts} +4 -3
- package/src/tools/{ens.ts → read/ens.ts} +27 -34
- package/src/tools/read/gas.ts +443 -0
- package/src/tools/read/logs.ts +281 -0
- package/src/tools/{signatures.ts → read/signatures.ts} +5 -5
- package/src/tools/read/signing.ts +92 -0
- package/src/tools/sign/errors.ts +28 -0
- package/src/tools/sign/transactions.ts +479 -0
- package/src/tools/sign/wallet.ts +216 -0
- package/src/utils.ts +52 -3
- package/src/wallet-client.ts +107 -24
- package/src/walletconnect.ts +250 -48
- package/dist/tools/advanced.d.ts.map +0 -1
- package/dist/tools/advanced.js.map +0 -1
- package/dist/tools/balance.d.ts.map +0 -1
- package/dist/tools/balance.js.map +0 -1
- package/dist/tools/contract-info.d.ts.map +0 -1
- package/dist/tools/contract-info.js.map +0 -1
- package/dist/tools/contract.d.ts.map +0 -1
- package/dist/tools/contract.js.map +0 -1
- package/dist/tools/ens.d.ts +0 -119
- package/dist/tools/ens.d.ts.map +0 -1
- package/dist/tools/ens.js.map +0 -1
- package/dist/tools/gas.d.ts +0 -99
- package/dist/tools/gas.d.ts.map +0 -1
- package/dist/tools/gas.js +0 -181
- package/dist/tools/gas.js.map +0 -1
- package/dist/tools/logs.d.ts +0 -39
- package/dist/tools/logs.d.ts.map +0 -1
- package/dist/tools/logs.js +0 -230
- package/dist/tools/logs.js.map +0 -1
- package/dist/tools/signatures.d.ts.map +0 -1
- package/dist/tools/signatures.js.map +0 -1
- package/dist/tools/transactions.d.ts +0 -155
- package/dist/tools/transactions.d.ts.map +0 -1
- package/dist/tools/transactions.js +0 -365
- package/dist/tools/transactions.js.map +0 -1
- package/src/tools/gas.ts +0 -219
- package/src/tools/logs.ts +0 -293
- package/src/tools/transactions.ts +0 -456
package/src/anvil.ts
CHANGED
|
@@ -1,226 +1,155 @@
|
|
|
1
|
-
import { type ChildProcess, spawn } from 'node:child_process'
|
|
2
1
|
import { createPublicClient, http } from 'viem'
|
|
3
2
|
import { mainnet } from 'viem/chains'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { HOSTED } from './hosted.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracing through a node that already supports `debug_*`, usually a local Anvil. Nothing
|
|
7
|
+
* is started here.
|
|
8
|
+
*
|
|
9
|
+
* Anvil serves no block below its own fork height — `debug_traceCall` at an earlier one
|
|
10
|
+
* fails with BlockOutOfRangeError — so replaying an old transaction means re-forking the
|
|
11
|
+
* node there with `anvil_reset`. That discards whatever state it held, which is why it
|
|
12
|
+
* happens only when ANVIL_ALLOW_RESET says so. A provider whose endpoint exposes
|
|
13
|
+
* `debug_traceTransaction` needs none of this and is tried first.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEFAULT_DEBUG_RPC = 'http://127.0.0.1:8545'
|
|
17
|
+
|
|
18
|
+
/** Re-forking clears the node, so it is never done to someone's dev chain uninvited. */
|
|
19
|
+
const ALLOW_RESET = ['1', 'true', 'yes'].includes((process.env.ANVIL_ALLOW_RESET ?? '').toLowerCase())
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The node to ask for traces, or undefined when there is none.
|
|
23
|
+
*
|
|
24
|
+
* Hosted, only an explicit setting counts: defaulting to loopback would turn every trace
|
|
25
|
+
* into a request against the server's own private network.
|
|
26
|
+
*/
|
|
27
|
+
export function debugRpcUrl(): string | undefined {
|
|
28
|
+
if (process.env.ANVIL_RPC_URL) return process.env.ANVIL_RPC_URL
|
|
29
|
+
return HOSTED ? undefined : DEFAULT_DEBUG_RPC
|
|
10
30
|
}
|
|
11
31
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
forkBlockNumber?: bigint
|
|
15
|
-
port?: number
|
|
16
|
-
chainId?: number
|
|
32
|
+
function debugClient(url: string) {
|
|
33
|
+
return createPublicClient({ chain: mainnet, transport: http(url) })
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
export async function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
proc.on('close', (code) => resolve(code === 0))
|
|
24
|
-
proc.on('error', () => resolve(false))
|
|
25
|
-
})
|
|
26
|
-
}
|
|
36
|
+
/** Whether a tracing node is reachable and actually offers the debug namespace. */
|
|
37
|
+
export async function isDebugNodeAvailable(): Promise<boolean> {
|
|
38
|
+
const url = debugRpcUrl()
|
|
39
|
+
if (!url) return false
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const address = server.address()
|
|
35
|
-
if (address && typeof address === 'object') {
|
|
36
|
-
server.close(() => resolve(address.port))
|
|
37
|
-
} else {
|
|
38
|
-
server.close(() => reject(new Error('Could not get port')))
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
server.on('error', () => {
|
|
42
|
-
// Port in use, try next one
|
|
43
|
-
findAvailablePort(startPort + 1)
|
|
44
|
-
.then(resolve)
|
|
45
|
-
.catch(reject)
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'web3_clientVersion', params: [], id: 1 }),
|
|
46
|
+
signal: AbortSignal.timeout(2000)
|
|
46
47
|
})
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Start an Anvil instance
|
|
51
|
-
export async function startAnvil(config: AnvilConfig): Promise<AnvilInstance> {
|
|
52
|
-
const installed = await isAnvilInstalled()
|
|
53
|
-
if (!installed) {
|
|
54
|
-
throw new Error('Anvil is not installed. Please install Foundry: https://book.getfoundry.sh/getting-started/installation')
|
|
48
|
+
return response.ok
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
55
51
|
}
|
|
52
|
+
}
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
export interface SimulateCallParams {
|
|
55
|
+
to: string
|
|
56
|
+
data?: string
|
|
57
|
+
from?: string
|
|
58
|
+
value?: bigint
|
|
59
|
+
gas?: bigint
|
|
60
|
+
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
args.push('--fork-block-number', config.forkBlockNumber.toString())
|
|
64
|
-
}
|
|
62
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
function callObject(params: SimulateCallParams) {
|
|
65
|
+
return {
|
|
66
|
+
to: params.to,
|
|
67
|
+
data: params.data,
|
|
68
|
+
from: params.from ?? ZERO_ADDRESS,
|
|
69
|
+
value: params.value ? `0x${params.value.toString(16)}` : undefined,
|
|
70
|
+
gas: params.gas ? `0x${params.gas.toString(16)}` : undefined
|
|
68
71
|
}
|
|
72
|
+
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
let stderr = ''
|
|
77
|
-
proc.stderr?.on('data', (data) => {
|
|
78
|
-
stderr += data.toString()
|
|
79
|
-
})
|
|
74
|
+
function blockTag(blockNumber?: bigint): string {
|
|
75
|
+
return blockNumber === undefined ? 'latest' : `0x${blockNumber.toString(16)}`
|
|
76
|
+
}
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
function requireDebugNode(): string {
|
|
79
|
+
const url = debugRpcUrl()
|
|
80
|
+
if (!url) {
|
|
81
|
+
throw new Error('No tracing node configured. Set ANVIL_RPC_URL to a node exposing debug_traceCall, such as a local Anvil.')
|
|
82
|
+
}
|
|
83
|
+
return url
|
|
84
|
+
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
})
|
|
86
|
+
function isBlockOutOfRange(error: unknown): boolean {
|
|
87
|
+
return /BlockOutOfRangeError|block height is/i.test((error as Error)?.message ?? '')
|
|
88
|
+
}
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
const maxAttempts = 50 // 5 seconds
|
|
93
|
-
let attempts = 0
|
|
90
|
+
type DebugClient = ReturnType<typeof debugClient>
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else if (attempts < maxAttempts) {
|
|
117
|
-
setTimeout(checkReady, 100)
|
|
118
|
-
} else {
|
|
119
|
-
proc.kill()
|
|
120
|
-
reject(new Error('Anvil failed to start within timeout'))
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
if (attempts < maxAttempts) {
|
|
124
|
-
setTimeout(checkReady, 100)
|
|
125
|
-
} else {
|
|
126
|
-
proc.kill()
|
|
127
|
-
reject(new Error('Anvil failed to start within timeout'))
|
|
128
|
-
}
|
|
129
|
-
}
|
|
92
|
+
/**
|
|
93
|
+
* Run something against a block, re-forking the node when it cannot reach that far back.
|
|
94
|
+
*
|
|
95
|
+
* Returns to head afterwards rather than leaving the node pinned to an old block, which
|
|
96
|
+
* would silently break whatever the owner does with it next. Its state is gone either way.
|
|
97
|
+
*/
|
|
98
|
+
async function atBlock<T>(
|
|
99
|
+
client: DebugClient,
|
|
100
|
+
forkUrl: string,
|
|
101
|
+
blockNumber: bigint | undefined,
|
|
102
|
+
run: (block: string) => Promise<T>
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
try {
|
|
105
|
+
return await run(blockTag(blockNumber))
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (blockNumber === undefined || !isBlockOutOfRange(error)) throw error
|
|
108
|
+
|
|
109
|
+
if (!ALLOW_RESET) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`The tracing node is forked past block ${blockNumber}, and Anvil serves nothing earlier. Re-forking it there would clear its state, so set ANVIL_ALLOW_RESET=1 to allow that — or use an RPC whose endpoint supports debug_traceTransaction.`
|
|
112
|
+
)
|
|
130
113
|
}
|
|
131
114
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Stop an Anvil instance
|
|
138
|
-
export function stopAnvil(instance: AnvilInstance): void {
|
|
139
|
-
if (instance.process && !instance.process.killed) {
|
|
140
|
-
instance.process.kill('SIGTERM')
|
|
141
|
-
}
|
|
142
|
-
}
|
|
115
|
+
const refork = (block?: bigint) =>
|
|
116
|
+
client.request({
|
|
117
|
+
method: 'anvil_reset' as never,
|
|
118
|
+
params: [{ forking: { jsonRpcUrl: forkUrl, ...(block === undefined ? {} : { blockNumber: Number(block) }) } }] as never
|
|
119
|
+
})
|
|
143
120
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
stopAnvil(instance)
|
|
121
|
+
await refork(blockNumber)
|
|
122
|
+
try {
|
|
123
|
+
return await run('latest')
|
|
124
|
+
} finally {
|
|
125
|
+
await refork().catch(() => {})
|
|
126
|
+
}
|
|
151
127
|
}
|
|
152
128
|
}
|
|
153
129
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
arbitrum: 'https://arbitrum-one.arpc.x.superfluid.dev'
|
|
130
|
+
function traceCall(client: DebugClient, call: ReturnType<typeof callObject>, block: string, tracer: string): Promise<unknown> {
|
|
131
|
+
return client.request({ method: 'debug_traceCall' as never, params: [call, block, { tracer }] as never })
|
|
157
132
|
}
|
|
158
133
|
|
|
159
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Replay a transaction through the tracing node.
|
|
136
|
+
*
|
|
137
|
+
* Run against the block before it, so the state is what the transaction actually saw. The
|
|
138
|
+
* node needs archive access to that block; without it this fails rather than quietly
|
|
139
|
+
* tracing against the wrong state.
|
|
140
|
+
*/
|
|
160
141
|
export async function traceTransactionWithAnvil(
|
|
161
142
|
forkUrl: string,
|
|
162
143
|
transactionHash: string,
|
|
163
144
|
blockNumber: bigint,
|
|
164
|
-
tracer: 'callTracer' | 'prestateTracer' | 'stateDiffTracer' = 'callTracer'
|
|
165
|
-
chainName?: string
|
|
145
|
+
tracer: 'callTracer' | 'prestateTracer' | 'stateDiffTracer' = 'callTracer'
|
|
166
146
|
): Promise<unknown> {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// First, fetch the original transaction from the RPC
|
|
171
|
-
const fetchClient = createPublicClient({
|
|
172
|
-
chain: mainnet,
|
|
173
|
-
transport: http(forkUrl)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const tx = await fetchClient.getTransaction({
|
|
177
|
-
hash: transactionHash as `0x${string}`
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
if (!tx) {
|
|
181
|
-
throw new Error(`Transaction ${transactionHash} not found`)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Fork at the block BEFORE the transaction to replay it accurately
|
|
185
|
-
// Note: This requires an archival RPC that has historical state
|
|
186
|
-
const forkBlock = blockNumber > 0n ? blockNumber - 1n : 0n
|
|
187
|
-
|
|
188
|
-
return withAnvil(
|
|
189
|
-
{
|
|
190
|
-
forkUrl: archiveUrl,
|
|
191
|
-
forkBlockNumber: forkBlock
|
|
192
|
-
},
|
|
193
|
-
async (instance) => {
|
|
194
|
-
// Use debug_traceCall to simulate and trace the transaction
|
|
195
|
-
// Note: This replays the tx against the post-block state, which may differ slightly
|
|
196
|
-
// from the original execution if state changed within the block
|
|
197
|
-
const trace = await instance.client.request({
|
|
198
|
-
method: 'debug_traceCall' as never,
|
|
199
|
-
params: [
|
|
200
|
-
{
|
|
201
|
-
from: tx.from,
|
|
202
|
-
to: tx.to,
|
|
203
|
-
data: tx.input,
|
|
204
|
-
value: tx.value ? `0x${tx.value.toString(16)}` : '0x0',
|
|
205
|
-
gas: tx.gas ? `0x${tx.gas.toString(16)}` : undefined
|
|
206
|
-
},
|
|
207
|
-
'latest',
|
|
208
|
-
{ tracer }
|
|
209
|
-
] as never
|
|
210
|
-
})
|
|
147
|
+
const tx = await debugClient(forkUrl).getTransaction({ hash: transactionHash as `0x${string}` })
|
|
148
|
+
if (!tx) throw new Error(`Transaction ${transactionHash} not found`)
|
|
211
149
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Simulate a call with tracing
|
|
218
|
-
export interface SimulateCallParams {
|
|
219
|
-
to: string
|
|
220
|
-
data?: string
|
|
221
|
-
from?: string
|
|
222
|
-
value?: bigint
|
|
223
|
-
gas?: bigint
|
|
150
|
+
const client = debugClient(requireDebugNode())
|
|
151
|
+
const call = callObject({ to: tx.to ?? ZERO_ADDRESS, data: tx.input, from: tx.from, value: tx.value, gas: tx.gas })
|
|
152
|
+
return atBlock(client, forkUrl, blockNumber > 0n ? blockNumber - 1n : 0n, (block) => traceCall(client, call, block, tracer))
|
|
224
153
|
}
|
|
225
154
|
|
|
226
155
|
export async function simulateCallWithTrace(
|
|
@@ -235,86 +164,36 @@ export async function simulateCallWithTrace(
|
|
|
235
164
|
success: boolean
|
|
236
165
|
revertReason?: string
|
|
237
166
|
}> {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
'latest'
|
|
263
|
-
] as never
|
|
264
|
-
})) as string
|
|
265
|
-
|
|
266
|
-
// Estimate gas
|
|
267
|
-
try {
|
|
268
|
-
gasUsed = await instance.client.estimateGas({
|
|
269
|
-
to: params.to as `0x${string}`,
|
|
270
|
-
data: params.data as `0x${string}` | undefined,
|
|
271
|
-
account: params.from as `0x${string}` | undefined,
|
|
272
|
-
value: params.value
|
|
273
|
-
})
|
|
274
|
-
} catch {
|
|
275
|
-
// Ignore gas estimation errors
|
|
276
|
-
}
|
|
277
|
-
} catch (error) {
|
|
278
|
-
success = false
|
|
279
|
-
const errorMessage = (error as Error).message
|
|
280
|
-
// Try to extract revert reason
|
|
281
|
-
const revertMatch = errorMessage.match(/revert(?:ed)?[:\s]*(.+?)(?:\n|$)/i)
|
|
282
|
-
if (revertMatch) {
|
|
283
|
-
revertReason = revertMatch[1].trim()
|
|
284
|
-
} else {
|
|
285
|
-
revertReason = errorMessage
|
|
286
|
-
}
|
|
287
|
-
}
|
|
167
|
+
const client = debugClient(requireDebugNode())
|
|
168
|
+
const call = callObject(params)
|
|
169
|
+
|
|
170
|
+
// Call, gas and trace all inside one atBlock, so they describe the same state: a block
|
|
171
|
+
// the node cannot serve re-forks it once, instead of being reported as a revert.
|
|
172
|
+
return atBlock(client, forkUrl, blockNumber, async (block) => {
|
|
173
|
+
let result = '0x'
|
|
174
|
+
let success = true
|
|
175
|
+
let revertReason: string | undefined
|
|
176
|
+
let gasUsed = 0n
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
result = (await client.request({ method: 'eth_call' as never, params: [call, block] as never })) as string
|
|
180
|
+
gasUsed = await client
|
|
181
|
+
.request({ method: 'eth_estimateGas' as never, params: [call, block] as never })
|
|
182
|
+
.then((gas) => BigInt(gas as string))
|
|
183
|
+
// Gas estimation is a nicety; a failure here says nothing about the call itself.
|
|
184
|
+
.catch(() => 0n)
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (isBlockOutOfRange(error)) throw error
|
|
187
|
+
success = false
|
|
188
|
+
const message = (error as Error).message
|
|
189
|
+
revertReason = message.match(/revert(?:ed)?[:\s]*(.+?)(?:\n|$)/i)?.[1]?.trim() ?? message
|
|
190
|
+
}
|
|
288
191
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
method: 'debug_traceCall' as never,
|
|
294
|
-
params: [
|
|
295
|
-
{
|
|
296
|
-
to: params.to,
|
|
297
|
-
data: params.data,
|
|
298
|
-
from: params.from ?? '0x0000000000000000000000000000000000000000',
|
|
299
|
-
value: params.value ? `0x${params.value.toString(16)}` : undefined,
|
|
300
|
-
gas: params.gas ? `0x${params.gas.toString(16)}` : undefined
|
|
301
|
-
},
|
|
302
|
-
'latest',
|
|
303
|
-
{ tracer }
|
|
304
|
-
] as never
|
|
305
|
-
})
|
|
306
|
-
} catch (traceError) {
|
|
307
|
-
// If tracing fails, include the error
|
|
308
|
-
trace = { error: (traceError as Error).message }
|
|
309
|
-
}
|
|
192
|
+
const trace = await traceCall(client, call, block, tracer).catch((error) => {
|
|
193
|
+
if (isBlockOutOfRange(error)) throw error
|
|
194
|
+
return { error: (error as Error).message }
|
|
195
|
+
})
|
|
310
196
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
trace,
|
|
314
|
-
gasUsed,
|
|
315
|
-
success,
|
|
316
|
-
revertReason
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
)
|
|
197
|
+
return { result, trace, gasUsed, success, revertReason }
|
|
198
|
+
})
|
|
320
199
|
}
|
package/src/chain-meta.ts
CHANGED
|
@@ -8,15 +8,21 @@ import { whatsabi } from '@shazow/whatsabi'
|
|
|
8
8
|
import { type Abi, type Address, parseAbiItem } from 'viem'
|
|
9
9
|
import { getClientManager } from './client.js'
|
|
10
10
|
import type { ChainName } from './types.js'
|
|
11
|
+
import { TtlCache } from './utils.js'
|
|
11
12
|
|
|
12
13
|
const ERC20_META_ABI = [
|
|
13
14
|
parseAbiItem('function decimals() view returns (uint8)'),
|
|
14
15
|
parseAbiItem('function symbol() view returns (string)')
|
|
15
16
|
]
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
// An hour covers a session's repeats while still noticing an upgraded proxy.
|
|
19
|
+
const TTL_MS = 60 * 60 * 1000
|
|
20
|
+
const abiCache = new TtlCache<{ abi: Abi; source: 'verified' | 'guessed'; proxy?: string; name?: string }>(200, TTL_MS)
|
|
21
|
+
const labelCache = new TtlCache<{ label?: string }>(1000, TTL_MS)
|
|
22
|
+
const metaCache = new TtlCache<{ symbol?: string; decimals?: number }>(1000, TTL_MS)
|
|
23
|
+
|
|
24
|
+
/** A local node is reset and redeployed at will, so nothing learned from it stays true. */
|
|
25
|
+
const cacheable = (chain: ChainName) => chain !== 'localhost'
|
|
20
26
|
|
|
21
27
|
export async function loadAbi(chain: ChainName, address: string) {
|
|
22
28
|
const key = `${chain}:${address.toLowerCase()}`
|
|
@@ -49,7 +55,7 @@ export async function loadAbi(chain: ChainName, address: string) {
|
|
|
49
55
|
proxy: result.address !== address ? result.address : undefined,
|
|
50
56
|
name: result.contractResult?.name ?? undefined
|
|
51
57
|
}
|
|
52
|
-
abiCache.set(key, loaded)
|
|
58
|
+
if (cacheable(chain)) abiCache.set(key, loaded)
|
|
53
59
|
return loaded
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -71,7 +77,8 @@ export async function tokenMeta(chain: ChainName, token: string) {
|
|
|
71
77
|
decimals: decimals.status === 'success' ? Number(decimals.result) : undefined,
|
|
72
78
|
symbol: symbol.status === 'success' ? (symbol.result as string) : undefined
|
|
73
79
|
}
|
|
74
|
-
|
|
80
|
+
// A failed call may be a flaky RPC rather than a token without that method.
|
|
81
|
+
if (cacheable(chain) && decimals.status === 'success' && symbol.status === 'success') metaCache.set(key, meta)
|
|
75
82
|
return meta
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -82,7 +89,8 @@ export async function tokenMeta(chain: ChainName, token: string) {
|
|
|
82
89
|
*/
|
|
83
90
|
export async function addressLabel(chain: ChainName, address: string): Promise<string | undefined> {
|
|
84
91
|
const key = `${chain}:${address.toLowerCase()}`
|
|
85
|
-
|
|
92
|
+
const cached = labelCache.get(key)
|
|
93
|
+
if (cached) return cached.label
|
|
86
94
|
|
|
87
95
|
let label: string | undefined
|
|
88
96
|
try {
|
|
@@ -95,9 +103,10 @@ export async function addressLabel(chain: ChainName, address: string): Promise<s
|
|
|
95
103
|
}
|
|
96
104
|
} catch {
|
|
97
105
|
// Unknown address — show it bare rather than failing the preview.
|
|
106
|
+
return undefined
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
labelCache.set(key, label)
|
|
109
|
+
if (cacheable(chain)) labelCache.set(key, { label })
|
|
101
110
|
return label
|
|
102
111
|
}
|
|
103
112
|
|
package/src/chains.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
unichain,
|
|
15
15
|
zksync
|
|
16
16
|
} from 'viem/chains'
|
|
17
|
+
import { HOSTED } from './hosted.js'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Every chain this server knows, in one table. Adding one here is the whole change: the
|
|
@@ -141,5 +142,16 @@ export type ChainName = keyof typeof TABLE
|
|
|
141
142
|
// literal entry would otherwise lose the fields that not every chain sets.
|
|
142
143
|
export const CHAINS: Record<ChainName, ChainInfo> = TABLE
|
|
143
144
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Chains the tools will accept.
|
|
147
|
+
*
|
|
148
|
+
* `localhost` is dropped when hosted. It resolves to the machine running the server, so on
|
|
149
|
+
* a shared host it is not the caller's dev node — it is a way to aim requests at whatever
|
|
150
|
+
* that host has listening on its own loopback.
|
|
151
|
+
*
|
|
152
|
+
* Tuple rather than array, because `z.enum` needs a non-empty literal list.
|
|
153
|
+
*/
|
|
154
|
+
export const SUPPORTED_CHAINS = (HOSTED ? Object.keys(CHAINS).filter((name) => name !== 'localhost') : Object.keys(CHAINS)) as [
|
|
155
|
+
ChainName,
|
|
156
|
+
...ChainName[]
|
|
157
|
+
]
|
package/src/clear-signing.ts
CHANGED
|
@@ -138,13 +138,19 @@ async function formatField(chain: ChainName, field: IndexField, root: unknown, t
|
|
|
138
138
|
const token =
|
|
139
139
|
field.params?.tokenAddress ?? (field.params?.tokenPath ? resolvePath(root, tx, field.params.tokenPath) : undefined)
|
|
140
140
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
141
|
+
const tokenAddress = typeof token === 'string' && isAddress(token) ? getAddress(token) : undefined
|
|
142
|
+
const meta = tokenAddress
|
|
143
|
+
? await tokenMeta(chain, tokenAddress).catch(() => ({ symbol: undefined, decimals: undefined }))
|
|
144
|
+
: { symbol: undefined, decimals: undefined }
|
|
145
|
+
// `amount` is the native currency, which always has 18 decimals.
|
|
146
|
+
const decimals = meta.decimals ?? (format === 'amount' && !tokenAddress ? 18 : undefined)
|
|
147
|
+
|
|
148
|
+
// symbol() is whatever the token contract says it is, so the address always goes with it.
|
|
149
|
+
const symbol = `${meta.symbol ? ` ${meta.symbol}` : ''}${tokenAddress ? ` (${tokenAddress})` : ''}`
|
|
150
|
+
if (tokenAddress) out.address = tokenAddress
|
|
151
|
+
if (amount > UNLIMITED_THRESHOLD) out.value = `Unlimited${symbol}`
|
|
152
|
+
else if (decimals === undefined) out.value = `${amount}${symbol} (decimals unknown)`
|
|
153
|
+
else out.value = `${formatUnits(amount, decimals)}${symbol}`
|
|
148
154
|
return out
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -156,10 +162,10 @@ async function formatField(chain: ChainName, field: IndexField, root: unknown, t
|
|
|
156
162
|
return out
|
|
157
163
|
}
|
|
158
164
|
const address = getAddress(raw as string)
|
|
159
|
-
const name =
|
|
165
|
+
const name = lookupContract(chain, address)?.protocol
|
|
160
166
|
out.address = address
|
|
161
167
|
out.name = name ? protocolLabel(name) : await addressLabel(chain, address).catch(() => undefined)
|
|
162
|
-
out.value = out.name
|
|
168
|
+
out.value = out.name ? `${out.name} (${address})` : address
|
|
163
169
|
return out
|
|
164
170
|
}
|
|
165
171
|
|
package/src/client.ts
CHANGED
package/src/context.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Who a tool call is acting for.
|
|
5
|
+
*
|
|
6
|
+
* A hosted server can carry several people, and signing must never cross between them: a
|
|
7
|
+
* transaction belongs to the person whose agent asked for it and reaches only their wallet.
|
|
8
|
+
*
|
|
9
|
+
* Passed explicitly from the tool wrapper down to whatever looks up a signer. Ambient
|
|
10
|
+
* storage would read better at the call sites and fail worse: a context lost across some
|
|
11
|
+
* later boundary would silently fall back to the shared identity, which is the same
|
|
12
|
+
* cross-tenant delivery the room boundary exists to prevent — and it would not show up in
|
|
13
|
+
* a test, because tests run inside the context.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Every self-hosted run, and every call arriving without an authenticated identity. */
|
|
17
|
+
export const DEFAULT_IDENTITY = 'default'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The identity an authenticated request belongs to. Hosted multi-tenancy is what fills
|
|
21
|
+
* this in; a single-credential server authenticates the deployment, not a person, so
|
|
22
|
+
* everyone on it is the same user.
|
|
23
|
+
*/
|
|
24
|
+
export function identityFrom(authInfo?: AuthInfo): string {
|
|
25
|
+
const address = authInfo?.extra?.address
|
|
26
|
+
return typeof address === 'string' && address ? address : DEFAULT_IDENTITY
|
|
27
|
+
}
|
package/src/hosted.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this process serves other people over HTTP, rather than one person on their own
|
|
3
|
+
* machine over stdio. It decides what a caller may reach: `localhost` as a chain, a tracing
|
|
4
|
+
* node on loopback, a browser to open. Every module that guards one of those reads it here.
|
|
5
|
+
*
|
|
6
|
+
* An environment variable rather than an argument, because the chain list is baked into the
|
|
7
|
+
* tool schemas as they are defined — so the answer has to exist before this package is
|
|
8
|
+
* imported, not when a server is started. The CLI sets MCP_HTTP_PORT; a host assembling its
|
|
9
|
+
* own server sets MCP_HOSTED=1, and startHttpServer refuses to run without one of them.
|
|
10
|
+
*/
|
|
11
|
+
export const HOSTED = Boolean(process.env.MCP_HTTP_PORT || process.env.MCP_HOSTED)
|