@thehoneyjar/sigil-dev-toolbar 0.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/LICENSE.md +660 -0
- package/dist/index.cjs +2404 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1099 -0
- package/dist/index.d.ts +1099 -0
- package/dist/index.js +2368 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2404 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var zustand = require('zustand');
|
|
5
|
+
var middleware = require('zustand/middleware');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
var wagmi = require('wagmi');
|
|
8
|
+
var viem = require('viem');
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __publicField = (obj, key, value) => {
|
|
13
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
16
|
+
var initialState = {
|
|
17
|
+
visible: true,
|
|
18
|
+
collapsed: true,
|
|
19
|
+
activeTab: "lens",
|
|
20
|
+
userLens: {
|
|
21
|
+
enabled: false,
|
|
22
|
+
impersonatedAddress: null,
|
|
23
|
+
savedAddresses: []
|
|
24
|
+
},
|
|
25
|
+
simulation: {
|
|
26
|
+
enabled: false,
|
|
27
|
+
scenario: null
|
|
28
|
+
},
|
|
29
|
+
comparison: {
|
|
30
|
+
enabled: false,
|
|
31
|
+
beforeState: null,
|
|
32
|
+
afterState: null
|
|
33
|
+
},
|
|
34
|
+
diagnostics: {
|
|
35
|
+
enabled: false,
|
|
36
|
+
violations: [],
|
|
37
|
+
tasteSignals: []
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var createDevToolbarStore = (config) => zustand.create()(
|
|
41
|
+
middleware.persist(
|
|
42
|
+
(set, get) => ({
|
|
43
|
+
...initialState,
|
|
44
|
+
collapsed: config.defaultCollapsed ?? true,
|
|
45
|
+
// Visibility
|
|
46
|
+
show: () => set({ visible: true }),
|
|
47
|
+
hide: () => set({ visible: false }),
|
|
48
|
+
toggle: () => set((state) => ({ visible: !state.visible })),
|
|
49
|
+
collapse: () => set({ collapsed: true }),
|
|
50
|
+
expand: () => set({ collapsed: false }),
|
|
51
|
+
setActiveTab: (tab) => set({ activeTab: tab, collapsed: false }),
|
|
52
|
+
// User Lens
|
|
53
|
+
enableLens: (address) => set({
|
|
54
|
+
userLens: {
|
|
55
|
+
...get().userLens,
|
|
56
|
+
enabled: true,
|
|
57
|
+
impersonatedAddress: address
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
disableLens: () => set({
|
|
61
|
+
userLens: {
|
|
62
|
+
...get().userLens,
|
|
63
|
+
enabled: false
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
setImpersonatedAddress: (address) => set({
|
|
67
|
+
userLens: {
|
|
68
|
+
...get().userLens,
|
|
69
|
+
impersonatedAddress: address,
|
|
70
|
+
enabled: address !== null
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
saveAddress: (entry) => set({
|
|
74
|
+
userLens: {
|
|
75
|
+
...get().userLens,
|
|
76
|
+
savedAddresses: [
|
|
77
|
+
...get().userLens.savedAddresses.filter((a) => a.address !== entry.address),
|
|
78
|
+
entry
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
removeAddress: (address) => set({
|
|
83
|
+
userLens: {
|
|
84
|
+
...get().userLens,
|
|
85
|
+
savedAddresses: get().userLens.savedAddresses.filter((a) => a.address !== address)
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
// Simulation
|
|
89
|
+
enableSimulation: (scenario) => set({
|
|
90
|
+
simulation: {
|
|
91
|
+
enabled: true,
|
|
92
|
+
scenario
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
disableSimulation: () => set({
|
|
96
|
+
simulation: {
|
|
97
|
+
enabled: false,
|
|
98
|
+
scenario: null
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
// Comparison
|
|
102
|
+
captureBeforeState: (state) => set({
|
|
103
|
+
comparison: {
|
|
104
|
+
...get().comparison,
|
|
105
|
+
enabled: true,
|
|
106
|
+
beforeState: state
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
captureAfterState: (state) => set({
|
|
110
|
+
comparison: {
|
|
111
|
+
...get().comparison,
|
|
112
|
+
afterState: state
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
clearComparison: () => set({
|
|
116
|
+
comparison: {
|
|
117
|
+
enabled: false,
|
|
118
|
+
beforeState: null,
|
|
119
|
+
afterState: null
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
// Diagnostics
|
|
123
|
+
addViolation: (violation) => set({
|
|
124
|
+
diagnostics: {
|
|
125
|
+
...get().diagnostics,
|
|
126
|
+
violations: [...get().diagnostics.violations.slice(-49), violation]
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
clearViolations: () => set({
|
|
130
|
+
diagnostics: {
|
|
131
|
+
...get().diagnostics,
|
|
132
|
+
violations: []
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
addTasteSignal: (signal) => {
|
|
136
|
+
set({
|
|
137
|
+
diagnostics: {
|
|
138
|
+
...get().diagnostics,
|
|
139
|
+
tasteSignals: [...get().diagnostics.tasteSignals.slice(-49), signal]
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
config.onTasteSignal?.(signal);
|
|
143
|
+
},
|
|
144
|
+
clearTasteSignals: () => set({
|
|
145
|
+
diagnostics: {
|
|
146
|
+
...get().diagnostics,
|
|
147
|
+
tasteSignals: []
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
// Reset
|
|
151
|
+
reset: () => set(initialState)
|
|
152
|
+
}),
|
|
153
|
+
{
|
|
154
|
+
name: "sigil-dev-toolbar",
|
|
155
|
+
partialize: (state) => ({
|
|
156
|
+
collapsed: state.collapsed,
|
|
157
|
+
activeTab: state.activeTab,
|
|
158
|
+
userLens: {
|
|
159
|
+
savedAddresses: state.userLens.savedAddresses
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
var DevToolbarContext = react.createContext(null);
|
|
166
|
+
var DevToolbarConfigContext = react.createContext(null);
|
|
167
|
+
var defaultConfig = {
|
|
168
|
+
position: "bottom-right",
|
|
169
|
+
defaultCollapsed: true,
|
|
170
|
+
enableUserLens: true,
|
|
171
|
+
enableSimulation: true,
|
|
172
|
+
enableComparison: true,
|
|
173
|
+
enableDiagnostics: true,
|
|
174
|
+
toggleShortcut: "ctrl+shift+d"
|
|
175
|
+
};
|
|
176
|
+
function DevToolbarProvider({ children, config: userConfig }) {
|
|
177
|
+
const config = react.useMemo(() => ({ ...defaultConfig, ...userConfig }), [userConfig]);
|
|
178
|
+
const store = react.useMemo(() => createDevToolbarStore(config), [config]);
|
|
179
|
+
react.useEffect(() => {
|
|
180
|
+
const handleKeyDown = (e) => {
|
|
181
|
+
const keys = config.toggleShortcut.toLowerCase().split("+");
|
|
182
|
+
const ctrlMatch = keys.includes("ctrl") === (e.ctrlKey || e.metaKey);
|
|
183
|
+
const shiftMatch = keys.includes("shift") === e.shiftKey;
|
|
184
|
+
const altMatch = keys.includes("alt") === e.altKey;
|
|
185
|
+
const keyMatch = keys.filter((k) => !["ctrl", "shift", "alt"].includes(k))[0] === e.key.toLowerCase();
|
|
186
|
+
if (ctrlMatch && shiftMatch && altMatch && keyMatch) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
store.getState().toggle();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
192
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
193
|
+
}, [config.toggleShortcut, store]);
|
|
194
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DevToolbarConfigContext.Provider, { value: config, children: /* @__PURE__ */ jsxRuntime.jsx(DevToolbarContext.Provider, { value: store, children }) });
|
|
195
|
+
}
|
|
196
|
+
function useDevToolbar() {
|
|
197
|
+
const store = react.useContext(DevToolbarContext);
|
|
198
|
+
if (!store) {
|
|
199
|
+
throw new Error("useDevToolbar must be used within a DevToolbarProvider");
|
|
200
|
+
}
|
|
201
|
+
return store();
|
|
202
|
+
}
|
|
203
|
+
function useDevToolbarSelector(selector) {
|
|
204
|
+
const store = react.useContext(DevToolbarContext);
|
|
205
|
+
if (!store) {
|
|
206
|
+
throw new Error("useDevToolbarSelector must be used within a DevToolbarProvider");
|
|
207
|
+
}
|
|
208
|
+
return store(selector);
|
|
209
|
+
}
|
|
210
|
+
function useDevToolbarConfig() {
|
|
211
|
+
const config = react.useContext(DevToolbarConfigContext);
|
|
212
|
+
if (!config) {
|
|
213
|
+
throw new Error("useDevToolbarConfig must be used within a DevToolbarProvider");
|
|
214
|
+
}
|
|
215
|
+
return config;
|
|
216
|
+
}
|
|
217
|
+
function useLensAwareAccount() {
|
|
218
|
+
const { address: realAddress, isConnected } = wagmi.useAccount();
|
|
219
|
+
const userLens = useDevToolbarSelector((state) => state.userLens);
|
|
220
|
+
const isImpersonating = userLens.enabled && userLens.impersonatedAddress !== null;
|
|
221
|
+
const address = isImpersonating ? userLens.impersonatedAddress : realAddress;
|
|
222
|
+
return {
|
|
223
|
+
address,
|
|
224
|
+
realAddress,
|
|
225
|
+
isImpersonating,
|
|
226
|
+
impersonatedAddress: userLens.impersonatedAddress,
|
|
227
|
+
isConnected
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function useIsImpersonating() {
|
|
231
|
+
return useDevToolbarSelector(
|
|
232
|
+
(state) => state.userLens.enabled && state.userLens.impersonatedAddress !== null
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function useImpersonatedAddress() {
|
|
236
|
+
return useDevToolbarSelector(
|
|
237
|
+
(state) => state.userLens.enabled ? state.userLens.impersonatedAddress : null
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
function useSavedAddresses() {
|
|
241
|
+
const savedAddresses = useDevToolbarSelector((state) => state.userLens.savedAddresses);
|
|
242
|
+
const { saveAddress, removeAddress, setImpersonatedAddress } = useDevToolbarSelector((state) => ({
|
|
243
|
+
saveAddress: state.saveAddress,
|
|
244
|
+
removeAddress: state.removeAddress,
|
|
245
|
+
setImpersonatedAddress: state.setImpersonatedAddress
|
|
246
|
+
}));
|
|
247
|
+
return {
|
|
248
|
+
savedAddresses,
|
|
249
|
+
saveAddress,
|
|
250
|
+
removeAddress,
|
|
251
|
+
selectAddress: setImpersonatedAddress
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/ipc/types.ts
|
|
256
|
+
var DEFAULT_IPC_CONFIG = {
|
|
257
|
+
basePath: "grimoires/pub",
|
|
258
|
+
timeout: 3e4,
|
|
259
|
+
pollInterval: 100
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/ipc/client.ts
|
|
263
|
+
function generateUUID() {
|
|
264
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
265
|
+
return crypto.randomUUID();
|
|
266
|
+
}
|
|
267
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
268
|
+
const r = Math.random() * 16 | 0;
|
|
269
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
270
|
+
return v.toString(16);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
var LocalStorageTransport = class {
|
|
274
|
+
constructor(prefix = "sigil-ipc") {
|
|
275
|
+
__publicField(this, "prefix");
|
|
276
|
+
this.prefix = prefix;
|
|
277
|
+
}
|
|
278
|
+
async writeRequest(request) {
|
|
279
|
+
const key = `${this.prefix}:request:${request.id}`;
|
|
280
|
+
localStorage.setItem(key, JSON.stringify(request));
|
|
281
|
+
}
|
|
282
|
+
async readResponse(requestId, cliType) {
|
|
283
|
+
const key = `${this.prefix}:response:${requestId}:${cliType}`;
|
|
284
|
+
const data = localStorage.getItem(key);
|
|
285
|
+
if (!data)
|
|
286
|
+
return null;
|
|
287
|
+
try {
|
|
288
|
+
return JSON.parse(data);
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async cleanup(requestId) {
|
|
294
|
+
const keysToRemove = [];
|
|
295
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
296
|
+
const key = localStorage.key(i);
|
|
297
|
+
if (key?.includes(requestId)) {
|
|
298
|
+
keysToRemove.push(key);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var MockTransport = class {
|
|
305
|
+
constructor() {
|
|
306
|
+
__publicField(this, "mockResponses", /* @__PURE__ */ new Map());
|
|
307
|
+
}
|
|
308
|
+
setMockResponse(requestId, cliType, response) {
|
|
309
|
+
this.mockResponses.set(`${requestId}:${cliType}`, response);
|
|
310
|
+
}
|
|
311
|
+
async writeRequest(_request) {
|
|
312
|
+
}
|
|
313
|
+
async readResponse(requestId, cliType) {
|
|
314
|
+
return this.mockResponses.get(`${requestId}:${cliType}`) ?? null;
|
|
315
|
+
}
|
|
316
|
+
async cleanup(_requestId) {
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var IPCClient = class {
|
|
320
|
+
constructor(transport, config) {
|
|
321
|
+
__publicField(this, "config");
|
|
322
|
+
__publicField(this, "transport");
|
|
323
|
+
this.config = { ...DEFAULT_IPC_CONFIG, ...config };
|
|
324
|
+
this.transport = transport ?? new LocalStorageTransport();
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Send a request and wait for response
|
|
328
|
+
*/
|
|
329
|
+
async send(type, payload, cliType = "anchor") {
|
|
330
|
+
const request = {
|
|
331
|
+
id: generateUUID(),
|
|
332
|
+
type,
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
payload
|
|
335
|
+
};
|
|
336
|
+
await this.transport.writeRequest(request);
|
|
337
|
+
const response = await this.waitForResponse(request.id, cliType);
|
|
338
|
+
await this.transport.cleanup(request.id);
|
|
339
|
+
if (response.status === "error") {
|
|
340
|
+
throw new Error(response.error ?? "Unknown IPC error");
|
|
341
|
+
}
|
|
342
|
+
if (response.status === "timeout") {
|
|
343
|
+
throw new Error("IPC request timed out");
|
|
344
|
+
}
|
|
345
|
+
return response.data;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Poll for response until timeout
|
|
349
|
+
*/
|
|
350
|
+
async waitForResponse(requestId, cliType) {
|
|
351
|
+
const startTime = Date.now();
|
|
352
|
+
while (Date.now() - startTime < this.config.timeout) {
|
|
353
|
+
const response = await this.transport.readResponse(requestId, cliType);
|
|
354
|
+
if (response) {
|
|
355
|
+
return response;
|
|
356
|
+
}
|
|
357
|
+
await new Promise((resolve) => setTimeout(resolve, this.config.pollInterval));
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
requestId,
|
|
361
|
+
status: "timeout",
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
error: "Request timed out"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Validate lens context via Anchor CLI
|
|
368
|
+
*/
|
|
369
|
+
async validateLensContext(context, zone) {
|
|
370
|
+
const payload = { context, zone };
|
|
371
|
+
return this.send("lens-validate", payload, "anchor");
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Run full Anchor validation with optional lens context
|
|
375
|
+
*/
|
|
376
|
+
async validateAnchor(statement, lensContext, zone) {
|
|
377
|
+
const payload = { statement, lensContext, zone };
|
|
378
|
+
return this.send("anchor-validate", payload, "anchor");
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get the current transport
|
|
382
|
+
*/
|
|
383
|
+
getTransport() {
|
|
384
|
+
return this.transport;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Set a new transport
|
|
388
|
+
*/
|
|
389
|
+
setTransport(transport) {
|
|
390
|
+
this.transport = transport;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
var defaultClient = null;
|
|
394
|
+
function getIPCClient() {
|
|
395
|
+
if (!defaultClient) {
|
|
396
|
+
defaultClient = new IPCClient();
|
|
397
|
+
}
|
|
398
|
+
return defaultClient;
|
|
399
|
+
}
|
|
400
|
+
function resetIPCClient() {
|
|
401
|
+
defaultClient = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/hooks/useIPCClient.ts
|
|
405
|
+
function useIPCClient() {
|
|
406
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
407
|
+
const [error, setError] = react.useState(null);
|
|
408
|
+
const clientRef = react.useRef(null);
|
|
409
|
+
const getClient = react.useCallback(() => {
|
|
410
|
+
if (!clientRef.current) {
|
|
411
|
+
clientRef.current = getIPCClient();
|
|
412
|
+
}
|
|
413
|
+
return clientRef.current;
|
|
414
|
+
}, []);
|
|
415
|
+
const validateLens = react.useCallback(
|
|
416
|
+
async (context, zone) => {
|
|
417
|
+
setIsLoading(true);
|
|
418
|
+
setError(null);
|
|
419
|
+
try {
|
|
420
|
+
const client = getClient();
|
|
421
|
+
const result = await client.validateLensContext(context, zone);
|
|
422
|
+
return result;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
425
|
+
setError(message);
|
|
426
|
+
throw err;
|
|
427
|
+
} finally {
|
|
428
|
+
setIsLoading(false);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
[getClient]
|
|
432
|
+
);
|
|
433
|
+
const validateAnchor = react.useCallback(
|
|
434
|
+
async (statement, lensContext, zone) => {
|
|
435
|
+
setIsLoading(true);
|
|
436
|
+
setError(null);
|
|
437
|
+
try {
|
|
438
|
+
const client = getClient();
|
|
439
|
+
const result = await client.validateAnchor(statement, lensContext, zone);
|
|
440
|
+
return result;
|
|
441
|
+
} catch (err) {
|
|
442
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
443
|
+
setError(message);
|
|
444
|
+
throw err;
|
|
445
|
+
} finally {
|
|
446
|
+
setIsLoading(false);
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
[getClient]
|
|
450
|
+
);
|
|
451
|
+
const clearError = react.useCallback(() => {
|
|
452
|
+
setError(null);
|
|
453
|
+
}, []);
|
|
454
|
+
return {
|
|
455
|
+
validateLens,
|
|
456
|
+
validateAnchor,
|
|
457
|
+
isLoading,
|
|
458
|
+
error,
|
|
459
|
+
clearError
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/services/fork.ts
|
|
464
|
+
function createAnvilForkService() {
|
|
465
|
+
let state = {
|
|
466
|
+
active: false,
|
|
467
|
+
rpcUrl: null,
|
|
468
|
+
blockNumber: null,
|
|
469
|
+
chainId: null,
|
|
470
|
+
createdAt: null,
|
|
471
|
+
snapshotCount: 0,
|
|
472
|
+
currentSnapshotId: null
|
|
473
|
+
};
|
|
474
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
475
|
+
async function jsonRpc(method, params = []) {
|
|
476
|
+
if (!state.rpcUrl) {
|
|
477
|
+
throw new Error("Fork not active");
|
|
478
|
+
}
|
|
479
|
+
const response = await fetch(state.rpcUrl, {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: { "Content-Type": "application/json" },
|
|
482
|
+
body: JSON.stringify({
|
|
483
|
+
jsonrpc: "2.0",
|
|
484
|
+
id: Date.now(),
|
|
485
|
+
method,
|
|
486
|
+
params
|
|
487
|
+
})
|
|
488
|
+
});
|
|
489
|
+
const data = await response.json();
|
|
490
|
+
if (data.error) {
|
|
491
|
+
throw new Error(data.error.message || "RPC error");
|
|
492
|
+
}
|
|
493
|
+
return data.result;
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
async createFork(config) {
|
|
497
|
+
const rpcUrl = config.customForkRpc || `http://127.0.0.1:${config.anvilPort || 8545}`;
|
|
498
|
+
try {
|
|
499
|
+
const chainIdHex = await fetch(rpcUrl, {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: { "Content-Type": "application/json" },
|
|
502
|
+
body: JSON.stringify({
|
|
503
|
+
jsonrpc: "2.0",
|
|
504
|
+
id: 1,
|
|
505
|
+
method: "eth_chainId",
|
|
506
|
+
params: []
|
|
507
|
+
})
|
|
508
|
+
}).then((r) => r.json()).then((d) => d.result);
|
|
509
|
+
const blockNumberHex = await fetch(rpcUrl, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: { "Content-Type": "application/json" },
|
|
512
|
+
body: JSON.stringify({
|
|
513
|
+
jsonrpc: "2.0",
|
|
514
|
+
id: 2,
|
|
515
|
+
method: "eth_blockNumber",
|
|
516
|
+
params: []
|
|
517
|
+
})
|
|
518
|
+
}).then((r) => r.json()).then((d) => d.result);
|
|
519
|
+
state = {
|
|
520
|
+
active: true,
|
|
521
|
+
rpcUrl,
|
|
522
|
+
blockNumber: BigInt(blockNumberHex),
|
|
523
|
+
chainId: parseInt(chainIdHex, 16),
|
|
524
|
+
createdAt: Date.now(),
|
|
525
|
+
snapshotCount: 0,
|
|
526
|
+
currentSnapshotId: null
|
|
527
|
+
};
|
|
528
|
+
return state;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
throw new Error(`Failed to connect to Anvil at ${rpcUrl}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
getState() {
|
|
534
|
+
return { ...state };
|
|
535
|
+
},
|
|
536
|
+
async snapshot(description) {
|
|
537
|
+
const result = await jsonRpc("evm_snapshot");
|
|
538
|
+
const id = result;
|
|
539
|
+
const blockNumberHex = await jsonRpc("eth_blockNumber");
|
|
540
|
+
const snapshot = {
|
|
541
|
+
id,
|
|
542
|
+
blockNumber: BigInt(blockNumberHex),
|
|
543
|
+
timestamp: Date.now(),
|
|
544
|
+
description
|
|
545
|
+
};
|
|
546
|
+
snapshots.set(id, snapshot);
|
|
547
|
+
state.snapshotCount++;
|
|
548
|
+
state.currentSnapshotId = id;
|
|
549
|
+
return snapshot;
|
|
550
|
+
},
|
|
551
|
+
async revert(snapshotId) {
|
|
552
|
+
const result = await jsonRpc("evm_revert", [snapshotId]);
|
|
553
|
+
if (result) {
|
|
554
|
+
state.currentSnapshotId = snapshotId;
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
},
|
|
558
|
+
async reset() {
|
|
559
|
+
await jsonRpc("anvil_reset");
|
|
560
|
+
snapshots.clear();
|
|
561
|
+
state.snapshotCount = 0;
|
|
562
|
+
state.currentSnapshotId = null;
|
|
563
|
+
},
|
|
564
|
+
async destroy() {
|
|
565
|
+
state = {
|
|
566
|
+
active: false,
|
|
567
|
+
rpcUrl: null,
|
|
568
|
+
blockNumber: null,
|
|
569
|
+
chainId: null,
|
|
570
|
+
createdAt: null,
|
|
571
|
+
snapshotCount: 0,
|
|
572
|
+
currentSnapshotId: null
|
|
573
|
+
};
|
|
574
|
+
snapshots.clear();
|
|
575
|
+
},
|
|
576
|
+
async setBalance(address, balance) {
|
|
577
|
+
await jsonRpc("anvil_setBalance", [address, `0x${balance.toString(16)}`]);
|
|
578
|
+
},
|
|
579
|
+
async impersonateAccount(address) {
|
|
580
|
+
await jsonRpc("anvil_impersonateAccount", [address]);
|
|
581
|
+
},
|
|
582
|
+
async stopImpersonating(address) {
|
|
583
|
+
await jsonRpc("anvil_stopImpersonatingAccount", [address]);
|
|
584
|
+
},
|
|
585
|
+
async mineBlock(blocks = 1) {
|
|
586
|
+
await jsonRpc("anvil_mine", [blocks]);
|
|
587
|
+
},
|
|
588
|
+
getRpcUrl() {
|
|
589
|
+
return state.rpcUrl;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function createTenderlyForkService() {
|
|
594
|
+
let state = {
|
|
595
|
+
active: false,
|
|
596
|
+
rpcUrl: null,
|
|
597
|
+
blockNumber: null,
|
|
598
|
+
chainId: null,
|
|
599
|
+
createdAt: null,
|
|
600
|
+
snapshotCount: 0,
|
|
601
|
+
currentSnapshotId: null
|
|
602
|
+
};
|
|
603
|
+
let forkId = null;
|
|
604
|
+
let config = null;
|
|
605
|
+
async function tenderlyApi(endpoint, method = "GET", body) {
|
|
606
|
+
if (!config?.tenderlyApiKey || !config?.tenderlyProject) {
|
|
607
|
+
throw new Error("Tenderly API key and project required");
|
|
608
|
+
}
|
|
609
|
+
const response = await fetch(`https://api.tenderly.co/api/v1/account/${config.tenderlyProject}/project/${config.tenderlyProject}${endpoint}`, {
|
|
610
|
+
method,
|
|
611
|
+
headers: {
|
|
612
|
+
"Content-Type": "application/json",
|
|
613
|
+
"X-Access-Key": config.tenderlyApiKey
|
|
614
|
+
},
|
|
615
|
+
body: body ? JSON.stringify(body) : void 0
|
|
616
|
+
});
|
|
617
|
+
if (!response.ok) {
|
|
618
|
+
throw new Error(`Tenderly API error: ${response.statusText}`);
|
|
619
|
+
}
|
|
620
|
+
return response.json();
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
async createFork(cfg) {
|
|
624
|
+
config = cfg;
|
|
625
|
+
const forkResponse = await tenderlyApi("/fork", "POST", {
|
|
626
|
+
network_id: cfg.chainId.toString(),
|
|
627
|
+
block_number: cfg.forkBlockNumber ? Number(cfg.forkBlockNumber) : void 0
|
|
628
|
+
});
|
|
629
|
+
forkId = forkResponse.simulation_fork.id;
|
|
630
|
+
state = {
|
|
631
|
+
active: true,
|
|
632
|
+
rpcUrl: forkResponse.simulation_fork.rpc_url,
|
|
633
|
+
blockNumber: BigInt(forkResponse.simulation_fork.block_number),
|
|
634
|
+
chainId: cfg.chainId,
|
|
635
|
+
createdAt: Date.now(),
|
|
636
|
+
snapshotCount: 0,
|
|
637
|
+
currentSnapshotId: null
|
|
638
|
+
};
|
|
639
|
+
return state;
|
|
640
|
+
},
|
|
641
|
+
getState() {
|
|
642
|
+
return { ...state };
|
|
643
|
+
},
|
|
644
|
+
async snapshot(description) {
|
|
645
|
+
if (!forkId)
|
|
646
|
+
throw new Error("Fork not active");
|
|
647
|
+
const response = await tenderlyApi(`/fork/${forkId}/snapshot`, "POST");
|
|
648
|
+
const snapshot = {
|
|
649
|
+
id: response.snapshot.id,
|
|
650
|
+
blockNumber: BigInt(response.snapshot.block_number),
|
|
651
|
+
timestamp: Date.now(),
|
|
652
|
+
description
|
|
653
|
+
};
|
|
654
|
+
state.snapshotCount++;
|
|
655
|
+
state.currentSnapshotId = snapshot.id;
|
|
656
|
+
return snapshot;
|
|
657
|
+
},
|
|
658
|
+
async revert(snapshotId) {
|
|
659
|
+
if (!forkId)
|
|
660
|
+
throw new Error("Fork not active");
|
|
661
|
+
try {
|
|
662
|
+
await tenderlyApi(`/fork/${forkId}/snapshot/${snapshotId}`, "PUT");
|
|
663
|
+
state.currentSnapshotId = snapshotId;
|
|
664
|
+
return true;
|
|
665
|
+
} catch {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
async reset() {
|
|
670
|
+
if (!forkId || !config)
|
|
671
|
+
throw new Error("Fork not active");
|
|
672
|
+
await tenderlyApi(`/fork/${forkId}`, "DELETE");
|
|
673
|
+
await this.createFork(config);
|
|
674
|
+
},
|
|
675
|
+
async destroy() {
|
|
676
|
+
if (forkId) {
|
|
677
|
+
try {
|
|
678
|
+
await tenderlyApi(`/fork/${forkId}`, "DELETE");
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
state = {
|
|
683
|
+
active: false,
|
|
684
|
+
rpcUrl: null,
|
|
685
|
+
blockNumber: null,
|
|
686
|
+
chainId: null,
|
|
687
|
+
createdAt: null,
|
|
688
|
+
snapshotCount: 0,
|
|
689
|
+
currentSnapshotId: null
|
|
690
|
+
};
|
|
691
|
+
forkId = null;
|
|
692
|
+
config = null;
|
|
693
|
+
},
|
|
694
|
+
async setBalance(address, balance) {
|
|
695
|
+
if (!state.rpcUrl)
|
|
696
|
+
throw new Error("Fork not active");
|
|
697
|
+
await fetch(state.rpcUrl, {
|
|
698
|
+
method: "POST",
|
|
699
|
+
headers: { "Content-Type": "application/json" },
|
|
700
|
+
body: JSON.stringify({
|
|
701
|
+
jsonrpc: "2.0",
|
|
702
|
+
id: Date.now(),
|
|
703
|
+
method: "tenderly_setBalance",
|
|
704
|
+
params: [address, `0x${balance.toString(16)}`]
|
|
705
|
+
})
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
async impersonateAccount(_address) {
|
|
709
|
+
},
|
|
710
|
+
async stopImpersonating(_address) {
|
|
711
|
+
},
|
|
712
|
+
async mineBlock(blocks = 1) {
|
|
713
|
+
if (!state.rpcUrl)
|
|
714
|
+
throw new Error("Fork not active");
|
|
715
|
+
await fetch(state.rpcUrl, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: { "Content-Type": "application/json" },
|
|
718
|
+
body: JSON.stringify({
|
|
719
|
+
jsonrpc: "2.0",
|
|
720
|
+
id: Date.now(),
|
|
721
|
+
method: "evm_increaseTime",
|
|
722
|
+
params: [blocks * 12]
|
|
723
|
+
// 12 seconds per block
|
|
724
|
+
})
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
getRpcUrl() {
|
|
728
|
+
return state.rpcUrl;
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function createForkService(provider) {
|
|
733
|
+
switch (provider) {
|
|
734
|
+
case "anvil":
|
|
735
|
+
return createAnvilForkService();
|
|
736
|
+
case "tenderly":
|
|
737
|
+
return createTenderlyForkService();
|
|
738
|
+
case "custom":
|
|
739
|
+
return createAnvilForkService();
|
|
740
|
+
default:
|
|
741
|
+
throw new Error(`Unknown fork provider: ${provider}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
var defaultForkService = null;
|
|
745
|
+
function getForkService(provider = "anvil") {
|
|
746
|
+
if (!defaultForkService) {
|
|
747
|
+
defaultForkService = createForkService(provider);
|
|
748
|
+
}
|
|
749
|
+
return defaultForkService;
|
|
750
|
+
}
|
|
751
|
+
function resetForkService() {
|
|
752
|
+
if (defaultForkService) {
|
|
753
|
+
defaultForkService.destroy();
|
|
754
|
+
defaultForkService = null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/hooks/useForkState.ts
|
|
759
|
+
function useForkState(defaultProvider = "anvil") {
|
|
760
|
+
const [state, setState] = react.useState({
|
|
761
|
+
active: false,
|
|
762
|
+
rpcUrl: null,
|
|
763
|
+
blockNumber: null,
|
|
764
|
+
chainId: null,
|
|
765
|
+
createdAt: null,
|
|
766
|
+
snapshotCount: 0,
|
|
767
|
+
currentSnapshotId: null
|
|
768
|
+
});
|
|
769
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
770
|
+
const [error, setError] = react.useState(null);
|
|
771
|
+
const [snapshots, setSnapshots] = react.useState([]);
|
|
772
|
+
const serviceRef = react.useRef(null);
|
|
773
|
+
react.useEffect(() => {
|
|
774
|
+
serviceRef.current = createForkService(defaultProvider);
|
|
775
|
+
return () => {
|
|
776
|
+
if (serviceRef.current) {
|
|
777
|
+
serviceRef.current.destroy();
|
|
778
|
+
serviceRef.current = null;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}, [defaultProvider]);
|
|
782
|
+
const createFork = react.useCallback(async (config) => {
|
|
783
|
+
if (!serviceRef.current) {
|
|
784
|
+
serviceRef.current = createForkService(config.provider);
|
|
785
|
+
}
|
|
786
|
+
setIsLoading(true);
|
|
787
|
+
setError(null);
|
|
788
|
+
try {
|
|
789
|
+
const newState = await serviceRef.current.createFork(config);
|
|
790
|
+
setState(newState);
|
|
791
|
+
setSnapshots([]);
|
|
792
|
+
} catch (err) {
|
|
793
|
+
const message = err instanceof Error ? err.message : "Failed to create fork";
|
|
794
|
+
setError(message);
|
|
795
|
+
throw err;
|
|
796
|
+
} finally {
|
|
797
|
+
setIsLoading(false);
|
|
798
|
+
}
|
|
799
|
+
}, []);
|
|
800
|
+
const snapshot = react.useCallback(async (description) => {
|
|
801
|
+
if (!serviceRef.current) {
|
|
802
|
+
setError("Fork service not initialized");
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
setIsLoading(true);
|
|
806
|
+
setError(null);
|
|
807
|
+
try {
|
|
808
|
+
const snap = await serviceRef.current.snapshot(description);
|
|
809
|
+
setSnapshots((prev) => [...prev, snap]);
|
|
810
|
+
setState(serviceRef.current.getState());
|
|
811
|
+
return snap;
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const message = err instanceof Error ? err.message : "Failed to take snapshot";
|
|
814
|
+
setError(message);
|
|
815
|
+
return null;
|
|
816
|
+
} finally {
|
|
817
|
+
setIsLoading(false);
|
|
818
|
+
}
|
|
819
|
+
}, []);
|
|
820
|
+
const revert = react.useCallback(async (snapshotId) => {
|
|
821
|
+
if (!serviceRef.current) {
|
|
822
|
+
setError("Fork service not initialized");
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
setIsLoading(true);
|
|
826
|
+
setError(null);
|
|
827
|
+
try {
|
|
828
|
+
const success = await serviceRef.current.revert(snapshotId);
|
|
829
|
+
if (success) {
|
|
830
|
+
setState(serviceRef.current.getState());
|
|
831
|
+
}
|
|
832
|
+
return success;
|
|
833
|
+
} catch (err) {
|
|
834
|
+
const message = err instanceof Error ? err.message : "Failed to revert";
|
|
835
|
+
setError(message);
|
|
836
|
+
return false;
|
|
837
|
+
} finally {
|
|
838
|
+
setIsLoading(false);
|
|
839
|
+
}
|
|
840
|
+
}, []);
|
|
841
|
+
const reset = react.useCallback(async () => {
|
|
842
|
+
if (!serviceRef.current) {
|
|
843
|
+
setError("Fork service not initialized");
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
setIsLoading(true);
|
|
847
|
+
setError(null);
|
|
848
|
+
try {
|
|
849
|
+
await serviceRef.current.reset();
|
|
850
|
+
setState(serviceRef.current.getState());
|
|
851
|
+
setSnapshots([]);
|
|
852
|
+
} catch (err) {
|
|
853
|
+
const message = err instanceof Error ? err.message : "Failed to reset";
|
|
854
|
+
setError(message);
|
|
855
|
+
} finally {
|
|
856
|
+
setIsLoading(false);
|
|
857
|
+
}
|
|
858
|
+
}, []);
|
|
859
|
+
const destroy = react.useCallback(async () => {
|
|
860
|
+
if (!serviceRef.current)
|
|
861
|
+
return;
|
|
862
|
+
setIsLoading(true);
|
|
863
|
+
setError(null);
|
|
864
|
+
try {
|
|
865
|
+
await serviceRef.current.destroy();
|
|
866
|
+
setState({
|
|
867
|
+
active: false,
|
|
868
|
+
rpcUrl: null,
|
|
869
|
+
blockNumber: null,
|
|
870
|
+
chainId: null,
|
|
871
|
+
createdAt: null,
|
|
872
|
+
snapshotCount: 0,
|
|
873
|
+
currentSnapshotId: null
|
|
874
|
+
});
|
|
875
|
+
setSnapshots([]);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
const message = err instanceof Error ? err.message : "Failed to destroy fork";
|
|
878
|
+
setError(message);
|
|
879
|
+
} finally {
|
|
880
|
+
setIsLoading(false);
|
|
881
|
+
}
|
|
882
|
+
}, []);
|
|
883
|
+
const setBalance = react.useCallback(async (address, balance) => {
|
|
884
|
+
if (!serviceRef.current) {
|
|
885
|
+
setError("Fork service not initialized");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
setIsLoading(true);
|
|
889
|
+
setError(null);
|
|
890
|
+
try {
|
|
891
|
+
await serviceRef.current.setBalance(address, balance);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
const message = err instanceof Error ? err.message : "Failed to set balance";
|
|
894
|
+
setError(message);
|
|
895
|
+
} finally {
|
|
896
|
+
setIsLoading(false);
|
|
897
|
+
}
|
|
898
|
+
}, []);
|
|
899
|
+
const impersonateAccount = react.useCallback(async (address) => {
|
|
900
|
+
if (!serviceRef.current) {
|
|
901
|
+
setError("Fork service not initialized");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
setIsLoading(true);
|
|
905
|
+
setError(null);
|
|
906
|
+
try {
|
|
907
|
+
await serviceRef.current.impersonateAccount(address);
|
|
908
|
+
} catch (err) {
|
|
909
|
+
const message = err instanceof Error ? err.message : "Failed to impersonate account";
|
|
910
|
+
setError(message);
|
|
911
|
+
} finally {
|
|
912
|
+
setIsLoading(false);
|
|
913
|
+
}
|
|
914
|
+
}, []);
|
|
915
|
+
const stopImpersonating = react.useCallback(async (address) => {
|
|
916
|
+
if (!serviceRef.current) {
|
|
917
|
+
setError("Fork service not initialized");
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
setIsLoading(true);
|
|
921
|
+
setError(null);
|
|
922
|
+
try {
|
|
923
|
+
await serviceRef.current.stopImpersonating(address);
|
|
924
|
+
} catch (err) {
|
|
925
|
+
const message = err instanceof Error ? err.message : "Failed to stop impersonating";
|
|
926
|
+
setError(message);
|
|
927
|
+
} finally {
|
|
928
|
+
setIsLoading(false);
|
|
929
|
+
}
|
|
930
|
+
}, []);
|
|
931
|
+
const mineBlock = react.useCallback(async (blocks = 1) => {
|
|
932
|
+
if (!serviceRef.current) {
|
|
933
|
+
setError("Fork service not initialized");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
setIsLoading(true);
|
|
937
|
+
setError(null);
|
|
938
|
+
try {
|
|
939
|
+
await serviceRef.current.mineBlock(blocks);
|
|
940
|
+
setState(serviceRef.current.getState());
|
|
941
|
+
} catch (err) {
|
|
942
|
+
const message = err instanceof Error ? err.message : "Failed to mine block";
|
|
943
|
+
setError(message);
|
|
944
|
+
} finally {
|
|
945
|
+
setIsLoading(false);
|
|
946
|
+
}
|
|
947
|
+
}, []);
|
|
948
|
+
return {
|
|
949
|
+
state,
|
|
950
|
+
isLoading,
|
|
951
|
+
error,
|
|
952
|
+
createFork,
|
|
953
|
+
snapshot,
|
|
954
|
+
revert,
|
|
955
|
+
reset,
|
|
956
|
+
destroy,
|
|
957
|
+
setBalance,
|
|
958
|
+
impersonateAccount,
|
|
959
|
+
stopImpersonating,
|
|
960
|
+
mineBlock,
|
|
961
|
+
snapshots
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/services/simulation.ts
|
|
966
|
+
function parseRevertReason(data) {
|
|
967
|
+
if (data.startsWith("0x08c379a0") && data.length >= 138) {
|
|
968
|
+
try {
|
|
969
|
+
const lengthHex = data.slice(74, 138);
|
|
970
|
+
const length = parseInt(lengthHex, 16);
|
|
971
|
+
if (length > 0 && length < 1e3) {
|
|
972
|
+
const messageHex = data.slice(138, 138 + length * 2);
|
|
973
|
+
const message = Buffer.from(messageHex, "hex").toString("utf8");
|
|
974
|
+
return message;
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (data.startsWith("0x4e487b71") && data.length >= 74) {
|
|
980
|
+
const panicCode = parseInt(data.slice(10, 74), 16);
|
|
981
|
+
const panicMessages = {
|
|
982
|
+
0: "Generic compiler panic",
|
|
983
|
+
1: "Assert failed",
|
|
984
|
+
17: "Arithmetic overflow/underflow",
|
|
985
|
+
18: "Division by zero",
|
|
986
|
+
33: "Invalid enum value",
|
|
987
|
+
34: "Invalid storage byte array",
|
|
988
|
+
49: "Pop on empty array",
|
|
989
|
+
50: "Array index out of bounds",
|
|
990
|
+
65: "Too much memory allocated",
|
|
991
|
+
81: "Internal function called"
|
|
992
|
+
};
|
|
993
|
+
return panicMessages[panicCode] || `Panic(0x${panicCode.toString(16)})`;
|
|
994
|
+
}
|
|
995
|
+
if (data.length > 10) {
|
|
996
|
+
const selector = data.slice(0, 10);
|
|
997
|
+
return `Custom error: ${selector}`;
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
function createSimulationService(forkService) {
|
|
1002
|
+
async function jsonRpc(method, params = []) {
|
|
1003
|
+
const rpcUrl = forkService.getRpcUrl();
|
|
1004
|
+
if (!rpcUrl) {
|
|
1005
|
+
throw new Error("Fork not active");
|
|
1006
|
+
}
|
|
1007
|
+
const response = await fetch(rpcUrl, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: { "Content-Type": "application/json" },
|
|
1010
|
+
body: JSON.stringify({
|
|
1011
|
+
jsonrpc: "2.0",
|
|
1012
|
+
id: Date.now(),
|
|
1013
|
+
method,
|
|
1014
|
+
params
|
|
1015
|
+
})
|
|
1016
|
+
});
|
|
1017
|
+
const data = await response.json();
|
|
1018
|
+
if (data.error) {
|
|
1019
|
+
throw new Error(data.error.message || "RPC error");
|
|
1020
|
+
}
|
|
1021
|
+
return data.result;
|
|
1022
|
+
}
|
|
1023
|
+
async function getBalance(address) {
|
|
1024
|
+
const result = await jsonRpc("eth_getBalance", [address, "latest"]);
|
|
1025
|
+
return BigInt(result);
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
async simulate(tx) {
|
|
1029
|
+
const timestamp = Date.now();
|
|
1030
|
+
const blockNumberHex = await jsonRpc("eth_blockNumber");
|
|
1031
|
+
const blockNumber = BigInt(blockNumberHex);
|
|
1032
|
+
const fromBalanceBefore = await getBalance(tx.from);
|
|
1033
|
+
const toBalanceBefore = await getBalance(tx.to);
|
|
1034
|
+
const gasLimit = tx.gas ?? await this.estimateGas(tx);
|
|
1035
|
+
const gasPrice = tx.gasPrice ?? tx.maxFeePerGas ?? await this.getGasPrice();
|
|
1036
|
+
const snapshot = await forkService.snapshot("pre-simulation");
|
|
1037
|
+
const txParams = {
|
|
1038
|
+
from: tx.from,
|
|
1039
|
+
to: tx.to,
|
|
1040
|
+
value: tx.value ? `0x${tx.value.toString(16)}` : "0x0",
|
|
1041
|
+
data: tx.data ?? "0x",
|
|
1042
|
+
gas: `0x${gasLimit.toString(16)}`,
|
|
1043
|
+
gasPrice: `0x${gasPrice.toString(16)}`,
|
|
1044
|
+
nonce: tx.nonce !== void 0 ? `0x${tx.nonce.toString(16)}` : void 0
|
|
1045
|
+
};
|
|
1046
|
+
let success = true;
|
|
1047
|
+
let hash;
|
|
1048
|
+
let gasUsed = 0n;
|
|
1049
|
+
let returnValue;
|
|
1050
|
+
let revertReason;
|
|
1051
|
+
const logs = [];
|
|
1052
|
+
try {
|
|
1053
|
+
await forkService.impersonateAccount(tx.from);
|
|
1054
|
+
hash = await jsonRpc("eth_sendTransaction", [txParams]);
|
|
1055
|
+
let receipt = null;
|
|
1056
|
+
let attempts = 0;
|
|
1057
|
+
while (!receipt && attempts < 50) {
|
|
1058
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1059
|
+
receipt = await jsonRpc("eth_getTransactionReceipt", [hash]);
|
|
1060
|
+
attempts++;
|
|
1061
|
+
}
|
|
1062
|
+
if (receipt) {
|
|
1063
|
+
const r = receipt;
|
|
1064
|
+
success = r.status === "0x1";
|
|
1065
|
+
gasUsed = BigInt(r.gasUsed);
|
|
1066
|
+
for (const log of r.logs) {
|
|
1067
|
+
logs.push({
|
|
1068
|
+
address: log.address,
|
|
1069
|
+
topics: log.topics,
|
|
1070
|
+
data: log.data
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (!success) {
|
|
1074
|
+
try {
|
|
1075
|
+
await jsonRpc("eth_call", [txParams, "latest"]);
|
|
1076
|
+
} catch (callError) {
|
|
1077
|
+
if (callError instanceof Error && callError.message) {
|
|
1078
|
+
revertReason = callError.message;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
success = false;
|
|
1085
|
+
if (error instanceof Error) {
|
|
1086
|
+
const errorData = error.data;
|
|
1087
|
+
if (errorData) {
|
|
1088
|
+
revertReason = parseRevertReason(errorData) ?? error.message;
|
|
1089
|
+
} else {
|
|
1090
|
+
revertReason = error.message;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
} finally {
|
|
1094
|
+
try {
|
|
1095
|
+
await forkService.stopImpersonating(tx.from);
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const fromBalanceAfter = await getBalance(tx.from);
|
|
1100
|
+
const toBalanceAfter = await getBalance(tx.to);
|
|
1101
|
+
const balanceChanges = [];
|
|
1102
|
+
if (fromBalanceBefore !== fromBalanceAfter) {
|
|
1103
|
+
balanceChanges.push({
|
|
1104
|
+
address: tx.from,
|
|
1105
|
+
token: null,
|
|
1106
|
+
symbol: "ETH",
|
|
1107
|
+
before: fromBalanceBefore,
|
|
1108
|
+
after: fromBalanceAfter,
|
|
1109
|
+
delta: fromBalanceAfter - fromBalanceBefore
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (tx.from !== tx.to && toBalanceBefore !== toBalanceAfter) {
|
|
1113
|
+
balanceChanges.push({
|
|
1114
|
+
address: tx.to,
|
|
1115
|
+
token: null,
|
|
1116
|
+
symbol: "ETH",
|
|
1117
|
+
before: toBalanceBefore,
|
|
1118
|
+
after: toBalanceAfter,
|
|
1119
|
+
delta: toBalanceAfter - toBalanceBefore
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
await forkService.revert(snapshot.id);
|
|
1123
|
+
const effectiveGasPrice = gasPrice;
|
|
1124
|
+
const totalCost = gasUsed * effectiveGasPrice + (tx.value ?? 0n);
|
|
1125
|
+
return {
|
|
1126
|
+
success,
|
|
1127
|
+
hash,
|
|
1128
|
+
gasUsed,
|
|
1129
|
+
gasLimit,
|
|
1130
|
+
effectiveGasPrice,
|
|
1131
|
+
totalCost,
|
|
1132
|
+
returnValue,
|
|
1133
|
+
revertReason,
|
|
1134
|
+
balanceChanges,
|
|
1135
|
+
stateChanges: [],
|
|
1136
|
+
// State changes require trace API
|
|
1137
|
+
logs,
|
|
1138
|
+
blockNumber,
|
|
1139
|
+
timestamp
|
|
1140
|
+
};
|
|
1141
|
+
},
|
|
1142
|
+
async estimateGas(tx) {
|
|
1143
|
+
const txParams = {
|
|
1144
|
+
from: tx.from,
|
|
1145
|
+
to: tx.to,
|
|
1146
|
+
value: tx.value ? `0x${tx.value.toString(16)}` : "0x0",
|
|
1147
|
+
data: tx.data ?? "0x"
|
|
1148
|
+
};
|
|
1149
|
+
try {
|
|
1150
|
+
const result = await jsonRpc("eth_estimateGas", [txParams]);
|
|
1151
|
+
const estimate = BigInt(result);
|
|
1152
|
+
return estimate * 120n / 100n;
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
return 21000n;
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
async getGasPrice() {
|
|
1158
|
+
const result = await jsonRpc("eth_gasPrice");
|
|
1159
|
+
return BigInt(result);
|
|
1160
|
+
},
|
|
1161
|
+
decodeRevertReason(data) {
|
|
1162
|
+
return parseRevertReason(data);
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
var defaultSimulationService = null;
|
|
1167
|
+
function getSimulationService(forkService) {
|
|
1168
|
+
if (!defaultSimulationService) {
|
|
1169
|
+
defaultSimulationService = createSimulationService(forkService);
|
|
1170
|
+
}
|
|
1171
|
+
return defaultSimulationService;
|
|
1172
|
+
}
|
|
1173
|
+
function resetSimulationService() {
|
|
1174
|
+
defaultSimulationService = null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/hooks/useTransactionSimulation.ts
|
|
1178
|
+
function useTransactionSimulation(config = {}) {
|
|
1179
|
+
const {
|
|
1180
|
+
provider = "anvil",
|
|
1181
|
+
forkUrl,
|
|
1182
|
+
chainId = 1,
|
|
1183
|
+
anvilPort = 8545,
|
|
1184
|
+
autoConnect = false,
|
|
1185
|
+
from
|
|
1186
|
+
} = config;
|
|
1187
|
+
const [result, setResult] = react.useState(null);
|
|
1188
|
+
const [isSimulating, setIsSimulating] = react.useState(false);
|
|
1189
|
+
const [isConnected, setIsConnected] = react.useState(false);
|
|
1190
|
+
const [isConnecting, setIsConnecting] = react.useState(false);
|
|
1191
|
+
const [error, setError] = react.useState(null);
|
|
1192
|
+
const forkServiceRef = react.useRef(null);
|
|
1193
|
+
const simulationServiceRef = react.useRef(null);
|
|
1194
|
+
const fromAddressRef = react.useRef(from);
|
|
1195
|
+
react.useEffect(() => {
|
|
1196
|
+
fromAddressRef.current = from;
|
|
1197
|
+
}, [from]);
|
|
1198
|
+
const connect = react.useCallback(async (overrideConfig) => {
|
|
1199
|
+
if (isConnected || isConnecting)
|
|
1200
|
+
return;
|
|
1201
|
+
setIsConnecting(true);
|
|
1202
|
+
setError(null);
|
|
1203
|
+
try {
|
|
1204
|
+
const forkConfig = {
|
|
1205
|
+
provider: overrideConfig?.provider ?? provider,
|
|
1206
|
+
forkUrl: overrideConfig?.forkUrl ?? forkUrl ?? "",
|
|
1207
|
+
chainId: overrideConfig?.chainId ?? chainId,
|
|
1208
|
+
anvilPort: overrideConfig?.anvilPort ?? anvilPort,
|
|
1209
|
+
customForkRpc: overrideConfig?.customForkRpc
|
|
1210
|
+
};
|
|
1211
|
+
forkServiceRef.current = createForkService(forkConfig.provider);
|
|
1212
|
+
await forkServiceRef.current.createFork(forkConfig);
|
|
1213
|
+
simulationServiceRef.current = createSimulationService(forkServiceRef.current);
|
|
1214
|
+
setIsConnected(true);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
const message = err instanceof Error ? err.message : "Failed to connect to fork";
|
|
1217
|
+
setError(message);
|
|
1218
|
+
forkServiceRef.current = null;
|
|
1219
|
+
simulationServiceRef.current = null;
|
|
1220
|
+
} finally {
|
|
1221
|
+
setIsConnecting(false);
|
|
1222
|
+
}
|
|
1223
|
+
}, [isConnected, isConnecting, provider, forkUrl, chainId, anvilPort]);
|
|
1224
|
+
const disconnect = react.useCallback(async () => {
|
|
1225
|
+
if (forkServiceRef.current) {
|
|
1226
|
+
await forkServiceRef.current.destroy();
|
|
1227
|
+
forkServiceRef.current = null;
|
|
1228
|
+
simulationServiceRef.current = null;
|
|
1229
|
+
}
|
|
1230
|
+
setIsConnected(false);
|
|
1231
|
+
setResult(null);
|
|
1232
|
+
setError(null);
|
|
1233
|
+
}, []);
|
|
1234
|
+
react.useEffect(() => {
|
|
1235
|
+
if (autoConnect) {
|
|
1236
|
+
connect();
|
|
1237
|
+
}
|
|
1238
|
+
return () => {
|
|
1239
|
+
if (forkServiceRef.current) {
|
|
1240
|
+
forkServiceRef.current.destroy();
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}, []);
|
|
1244
|
+
const simulate = react.useCallback(async (tx) => {
|
|
1245
|
+
if (!simulationServiceRef.current || !forkServiceRef.current) {
|
|
1246
|
+
setError("Not connected to fork. Call connect() first.");
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
const senderAddress = fromAddressRef.current;
|
|
1250
|
+
if (!senderAddress) {
|
|
1251
|
+
setError('No sender address provided. Set "from" in config.');
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
setIsSimulating(true);
|
|
1255
|
+
setError(null);
|
|
1256
|
+
try {
|
|
1257
|
+
const request = {
|
|
1258
|
+
from: senderAddress,
|
|
1259
|
+
to: tx.to,
|
|
1260
|
+
data: tx.data,
|
|
1261
|
+
value: tx.value,
|
|
1262
|
+
gas: tx.gas
|
|
1263
|
+
};
|
|
1264
|
+
const simResult = await simulationServiceRef.current.simulate(request);
|
|
1265
|
+
setResult(simResult);
|
|
1266
|
+
return simResult;
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
const message = err instanceof Error ? err.message : "Simulation failed";
|
|
1269
|
+
setError(message);
|
|
1270
|
+
return null;
|
|
1271
|
+
} finally {
|
|
1272
|
+
setIsSimulating(false);
|
|
1273
|
+
}
|
|
1274
|
+
}, []);
|
|
1275
|
+
const clearResult = react.useCallback(() => {
|
|
1276
|
+
setResult(null);
|
|
1277
|
+
setError(null);
|
|
1278
|
+
}, []);
|
|
1279
|
+
const reset = react.useCallback(async () => {
|
|
1280
|
+
if (forkServiceRef.current) {
|
|
1281
|
+
await forkServiceRef.current.reset();
|
|
1282
|
+
setResult(null);
|
|
1283
|
+
setError(null);
|
|
1284
|
+
}
|
|
1285
|
+
}, []);
|
|
1286
|
+
return {
|
|
1287
|
+
result,
|
|
1288
|
+
isSimulating,
|
|
1289
|
+
isConnected,
|
|
1290
|
+
isConnecting,
|
|
1291
|
+
error,
|
|
1292
|
+
simulate,
|
|
1293
|
+
connect,
|
|
1294
|
+
disconnect,
|
|
1295
|
+
clearResult,
|
|
1296
|
+
reset
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function useSimulation(from, options = {}) {
|
|
1300
|
+
const hook = useTransactionSimulation({
|
|
1301
|
+
...options,
|
|
1302
|
+
from,
|
|
1303
|
+
autoConnect: true
|
|
1304
|
+
});
|
|
1305
|
+
const { connect: _connect, disconnect: _disconnect, ...rest } = hook;
|
|
1306
|
+
return rest;
|
|
1307
|
+
}
|
|
1308
|
+
function isValidAddressInput(input) {
|
|
1309
|
+
if (viem.isAddress(input))
|
|
1310
|
+
return true;
|
|
1311
|
+
if (input.endsWith(".eth") && input.length > 4)
|
|
1312
|
+
return true;
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
function truncateAddress(address) {
|
|
1316
|
+
if (address.length <= 10)
|
|
1317
|
+
return address;
|
|
1318
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1319
|
+
}
|
|
1320
|
+
function UserLens() {
|
|
1321
|
+
const [input, setInput] = react.useState("");
|
|
1322
|
+
const [error, setError] = react.useState(null);
|
|
1323
|
+
const [isResolving, setIsResolving] = react.useState(false);
|
|
1324
|
+
const { setImpersonatedAddress, disableLens, saveAddress, userLens } = useDevToolbar();
|
|
1325
|
+
const { savedAddresses, selectAddress, removeAddress } = useSavedAddresses();
|
|
1326
|
+
const isImpersonating = useIsImpersonating();
|
|
1327
|
+
const handleSubmit = react.useCallback(
|
|
1328
|
+
async (e) => {
|
|
1329
|
+
e.preventDefault();
|
|
1330
|
+
setError(null);
|
|
1331
|
+
const trimmed = input.trim();
|
|
1332
|
+
if (!trimmed)
|
|
1333
|
+
return;
|
|
1334
|
+
if (!isValidAddressInput(trimmed)) {
|
|
1335
|
+
setError("Invalid address or ENS name");
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (viem.isAddress(trimmed)) {
|
|
1339
|
+
setImpersonatedAddress(trimmed);
|
|
1340
|
+
setInput("");
|
|
1341
|
+
} else if (trimmed.endsWith(".eth")) {
|
|
1342
|
+
setIsResolving(true);
|
|
1343
|
+
setError("ENS resolution not yet implemented. Please use address.");
|
|
1344
|
+
setIsResolving(false);
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
[input, setImpersonatedAddress]
|
|
1348
|
+
);
|
|
1349
|
+
const handleClear = react.useCallback(() => {
|
|
1350
|
+
disableLens();
|
|
1351
|
+
setInput("");
|
|
1352
|
+
setError(null);
|
|
1353
|
+
}, [disableLens]);
|
|
1354
|
+
const handleSaveCurrentAddress = react.useCallback(() => {
|
|
1355
|
+
const trimmed = input.trim();
|
|
1356
|
+
if (viem.isAddress(trimmed)) {
|
|
1357
|
+
saveAddress({
|
|
1358
|
+
address: trimmed,
|
|
1359
|
+
label: `Address ${savedAddresses.length + 1}`
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
}, [input, saveAddress, savedAddresses.length]);
|
|
1363
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-user-lens", children: [
|
|
1364
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-user-lens__header", children: [
|
|
1365
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: "User Lens" }),
|
|
1366
|
+
isImpersonating && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-user-lens__badge", children: "Active" })
|
|
1367
|
+
] }),
|
|
1368
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "sigil-user-lens__description", children: "View the app as another address. Reads use the impersonated address, transactions still sign with your real wallet." }),
|
|
1369
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "sigil-user-lens__form", children: [
|
|
1370
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-user-lens__input-group", children: [
|
|
1371
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1372
|
+
"input",
|
|
1373
|
+
{
|
|
1374
|
+
type: "text",
|
|
1375
|
+
value: input,
|
|
1376
|
+
onChange: (e) => setInput(e.target.value),
|
|
1377
|
+
placeholder: "0x... or name.eth",
|
|
1378
|
+
className: "sigil-user-lens__input",
|
|
1379
|
+
disabled: isResolving
|
|
1380
|
+
}
|
|
1381
|
+
),
|
|
1382
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1383
|
+
"button",
|
|
1384
|
+
{
|
|
1385
|
+
type: "submit",
|
|
1386
|
+
className: "sigil-user-lens__button sigil-user-lens__button--primary",
|
|
1387
|
+
disabled: isResolving || !input.trim(),
|
|
1388
|
+
children: isResolving ? "Resolving..." : "Impersonate"
|
|
1389
|
+
}
|
|
1390
|
+
)
|
|
1391
|
+
] }),
|
|
1392
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sigil-user-lens__error", children: error })
|
|
1393
|
+
] }),
|
|
1394
|
+
isImpersonating && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-user-lens__active", children: [
|
|
1395
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-user-lens__active-header", children: [
|
|
1396
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Currently viewing as:" }),
|
|
1397
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1398
|
+
"button",
|
|
1399
|
+
{
|
|
1400
|
+
onClick: handleClear,
|
|
1401
|
+
className: "sigil-user-lens__button sigil-user-lens__button--danger",
|
|
1402
|
+
children: "Clear"
|
|
1403
|
+
}
|
|
1404
|
+
)
|
|
1405
|
+
] }),
|
|
1406
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "sigil-user-lens__address", children: userLens.impersonatedAddress })
|
|
1407
|
+
] }),
|
|
1408
|
+
savedAddresses.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-user-lens__saved", children: [
|
|
1409
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Saved Addresses" }),
|
|
1410
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "sigil-user-lens__saved-list", children: savedAddresses.map((entry) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-user-lens__saved-item", children: [
|
|
1411
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1412
|
+
"button",
|
|
1413
|
+
{
|
|
1414
|
+
onClick: () => selectAddress(entry.address),
|
|
1415
|
+
className: "sigil-user-lens__saved-button",
|
|
1416
|
+
children: [
|
|
1417
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-user-lens__saved-label", children: entry.label }),
|
|
1418
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-user-lens__saved-address", children: truncateAddress(entry.address) })
|
|
1419
|
+
]
|
|
1420
|
+
}
|
|
1421
|
+
),
|
|
1422
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1423
|
+
"button",
|
|
1424
|
+
{
|
|
1425
|
+
onClick: () => removeAddress(entry.address),
|
|
1426
|
+
className: "sigil-user-lens__remove",
|
|
1427
|
+
"aria-label": "Remove",
|
|
1428
|
+
children: "\xD7"
|
|
1429
|
+
}
|
|
1430
|
+
)
|
|
1431
|
+
] }, entry.address)) })
|
|
1432
|
+
] }),
|
|
1433
|
+
input && viem.isAddress(input) && !savedAddresses.find((a) => a.address === input) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1434
|
+
"button",
|
|
1435
|
+
{
|
|
1436
|
+
onClick: handleSaveCurrentAddress,
|
|
1437
|
+
className: "sigil-user-lens__button sigil-user-lens__button--secondary",
|
|
1438
|
+
children: "Save Address"
|
|
1439
|
+
}
|
|
1440
|
+
)
|
|
1441
|
+
] });
|
|
1442
|
+
}
|
|
1443
|
+
function LensActiveBadge() {
|
|
1444
|
+
const isImpersonating = useIsImpersonating();
|
|
1445
|
+
if (!isImpersonating)
|
|
1446
|
+
return null;
|
|
1447
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-lens-badge", children: [
|
|
1448
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-lens-badge__icon", children: "\u{1F441}" }),
|
|
1449
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-lens-badge__text", children: "Lens Active" })
|
|
1450
|
+
] });
|
|
1451
|
+
}
|
|
1452
|
+
var TABS = [
|
|
1453
|
+
{ id: "lens", label: "Lens", icon: "\u{1F441}", configKey: "enableUserLens" },
|
|
1454
|
+
{ id: "simulate", label: "Simulate", icon: "\u{1F3AD}", configKey: "enableSimulation" },
|
|
1455
|
+
{ id: "compare", label: "Compare", icon: "\u2696\uFE0F", configKey: "enableComparison" },
|
|
1456
|
+
{ id: "diagnose", label: "Diagnose", icon: "\u{1F50D}", configKey: "enableDiagnostics" }
|
|
1457
|
+
];
|
|
1458
|
+
function PlaceholderPanel({ title }) {
|
|
1459
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-toolbar__placeholder", children: [
|
|
1460
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: title }),
|
|
1461
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Coming in Sprint 4+" })
|
|
1462
|
+
] });
|
|
1463
|
+
}
|
|
1464
|
+
function TabContent({ tab }) {
|
|
1465
|
+
switch (tab) {
|
|
1466
|
+
case "lens":
|
|
1467
|
+
return /* @__PURE__ */ jsxRuntime.jsx(UserLens, {});
|
|
1468
|
+
case "simulate":
|
|
1469
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PlaceholderPanel, { title: "Simulation" });
|
|
1470
|
+
case "compare":
|
|
1471
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PlaceholderPanel, { title: "State Comparison" });
|
|
1472
|
+
case "diagnose":
|
|
1473
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PlaceholderPanel, { title: "Diagnostics" });
|
|
1474
|
+
default:
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function DevToolbar() {
|
|
1479
|
+
const config = useDevToolbarConfig();
|
|
1480
|
+
const {
|
|
1481
|
+
visible,
|
|
1482
|
+
collapsed,
|
|
1483
|
+
activeTab,
|
|
1484
|
+
setActiveTab,
|
|
1485
|
+
collapse,
|
|
1486
|
+
expand,
|
|
1487
|
+
toggle,
|
|
1488
|
+
userLens
|
|
1489
|
+
} = useDevToolbar();
|
|
1490
|
+
const handleTabClick = react.useCallback(
|
|
1491
|
+
(tab) => {
|
|
1492
|
+
if (collapsed) {
|
|
1493
|
+
expand();
|
|
1494
|
+
}
|
|
1495
|
+
setActiveTab(tab);
|
|
1496
|
+
},
|
|
1497
|
+
[collapsed, expand, setActiveTab]
|
|
1498
|
+
);
|
|
1499
|
+
const handleToggleCollapse = react.useCallback(() => {
|
|
1500
|
+
if (collapsed) {
|
|
1501
|
+
expand();
|
|
1502
|
+
} else {
|
|
1503
|
+
collapse();
|
|
1504
|
+
}
|
|
1505
|
+
}, [collapsed, collapse, expand]);
|
|
1506
|
+
if (process.env.NODE_ENV === "production") {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
if (!visible) {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
const enabledTabs = TABS.filter((tab) => config[tab.configKey]);
|
|
1513
|
+
const positionClass = `sigil-toolbar--${config.position}`;
|
|
1514
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `sigil-toolbar ${positionClass} ${collapsed ? "sigil-toolbar--collapsed" : ""}`, children: [
|
|
1515
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-toolbar__header", children: [
|
|
1516
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-toolbar__tabs", children: enabledTabs.map((tab) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1517
|
+
"button",
|
|
1518
|
+
{
|
|
1519
|
+
onClick: () => handleTabClick(tab.id),
|
|
1520
|
+
className: `sigil-toolbar__tab ${activeTab === tab.id ? "sigil-toolbar__tab--active" : ""}`,
|
|
1521
|
+
title: tab.label,
|
|
1522
|
+
children: [
|
|
1523
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-toolbar__tab-icon", children: tab.icon }),
|
|
1524
|
+
!collapsed && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-toolbar__tab-label", children: tab.label })
|
|
1525
|
+
]
|
|
1526
|
+
},
|
|
1527
|
+
tab.id
|
|
1528
|
+
)) }),
|
|
1529
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-toolbar__controls", children: [
|
|
1530
|
+
userLens.enabled && collapsed && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-toolbar__lens-indicator", title: "Lens Active", children: "\u{1F441}" }),
|
|
1531
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1532
|
+
"button",
|
|
1533
|
+
{
|
|
1534
|
+
onClick: handleToggleCollapse,
|
|
1535
|
+
className: "sigil-toolbar__collapse-btn",
|
|
1536
|
+
"aria-label": collapsed ? "Expand toolbar" : "Collapse toolbar",
|
|
1537
|
+
children: collapsed ? "\u25C0" : "\u25B6"
|
|
1538
|
+
}
|
|
1539
|
+
),
|
|
1540
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1541
|
+
"button",
|
|
1542
|
+
{
|
|
1543
|
+
onClick: toggle,
|
|
1544
|
+
className: "sigil-toolbar__close-btn",
|
|
1545
|
+
"aria-label": "Close toolbar",
|
|
1546
|
+
children: "\xD7"
|
|
1547
|
+
}
|
|
1548
|
+
)
|
|
1549
|
+
] })
|
|
1550
|
+
] }),
|
|
1551
|
+
!collapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-toolbar__content", children: /* @__PURE__ */ jsxRuntime.jsx(TabContent, { tab: activeTab }) }),
|
|
1552
|
+
collapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-toolbar__shortcut-hint", children: config.toggleShortcut })
|
|
1553
|
+
] });
|
|
1554
|
+
}
|
|
1555
|
+
function DevToolbarTrigger() {
|
|
1556
|
+
const { visible, show } = useDevToolbar();
|
|
1557
|
+
const config = useDevToolbarConfig();
|
|
1558
|
+
if (process.env.NODE_ENV === "production") {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
if (visible) {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
const positionClass = `sigil-toolbar-trigger--${config.position}`;
|
|
1565
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1566
|
+
"button",
|
|
1567
|
+
{
|
|
1568
|
+
onClick: show,
|
|
1569
|
+
className: `sigil-toolbar-trigger ${positionClass}`,
|
|
1570
|
+
"aria-label": "Open Sigil Dev Toolbar",
|
|
1571
|
+
title: `Sigil Dev Toolbar (${config.toggleShortcut})`,
|
|
1572
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u26A1" })
|
|
1573
|
+
}
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
function DevToolbarWithTrigger() {
|
|
1577
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1578
|
+
/* @__PURE__ */ jsxRuntime.jsx(DevToolbar, {}),
|
|
1579
|
+
/* @__PURE__ */ jsxRuntime.jsx(DevToolbarTrigger, {}),
|
|
1580
|
+
/* @__PURE__ */ jsxRuntime.jsx(LensActiveBadge, {})
|
|
1581
|
+
] });
|
|
1582
|
+
}
|
|
1583
|
+
function generateQuestions(issues) {
|
|
1584
|
+
return issues.map((issue, index) => {
|
|
1585
|
+
let question;
|
|
1586
|
+
let severity;
|
|
1587
|
+
switch (issue.type) {
|
|
1588
|
+
case "data_source_mismatch":
|
|
1589
|
+
question = `The ${issue.component} shows "${issue.actual}" but on-chain value is "${issue.expected}". Which is correct?`;
|
|
1590
|
+
severity = issue.severity === "error" ? "critical" : "important";
|
|
1591
|
+
break;
|
|
1592
|
+
case "stale_indexed_data":
|
|
1593
|
+
question = `The indexer shows stale data for ${issue.component}. Expected: "${issue.expected}", Got: "${issue.actual}". Has this been recently updated?`;
|
|
1594
|
+
severity = issue.severity === "error" ? "critical" : "info";
|
|
1595
|
+
break;
|
|
1596
|
+
case "lens_financial_check":
|
|
1597
|
+
question = `${issue.component} uses indexed data for financial operations. Should this use on-chain data instead?`;
|
|
1598
|
+
severity = "critical";
|
|
1599
|
+
break;
|
|
1600
|
+
case "impersonation_leak":
|
|
1601
|
+
question = `${issue.component} appears to show the real address instead of the impersonated one. Is this intentional?`;
|
|
1602
|
+
severity = "critical";
|
|
1603
|
+
break;
|
|
1604
|
+
default:
|
|
1605
|
+
question = issue.message;
|
|
1606
|
+
severity = "info";
|
|
1607
|
+
}
|
|
1608
|
+
return {
|
|
1609
|
+
id: `q-${index}`,
|
|
1610
|
+
question,
|
|
1611
|
+
context: issue.suggestion ?? "",
|
|
1612
|
+
severity
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
function DiagnosticPanel({
|
|
1617
|
+
items: initialItems = [],
|
|
1618
|
+
autoQuery = false,
|
|
1619
|
+
onDiagnosisComplete
|
|
1620
|
+
}) {
|
|
1621
|
+
const [items, setItems] = react.useState(initialItems);
|
|
1622
|
+
const [questions, setQuestions] = react.useState([]);
|
|
1623
|
+
const [isQuerying, setIsQuerying] = react.useState(false);
|
|
1624
|
+
const { validateLens, isLoading, error } = useIPCClient();
|
|
1625
|
+
const { address, isImpersonating, realAddress, impersonatedAddress } = useLensAwareAccount();
|
|
1626
|
+
const diagnosticsState = useDevToolbarSelector((state) => state.diagnostics);
|
|
1627
|
+
const { addViolation } = useDevToolbar();
|
|
1628
|
+
const runDiagnostics = react.useCallback(async () => {
|
|
1629
|
+
if (!address)
|
|
1630
|
+
return;
|
|
1631
|
+
setIsQuerying(true);
|
|
1632
|
+
const newIssues = [];
|
|
1633
|
+
const updatedItems = [...items];
|
|
1634
|
+
for (let i = 0; i < updatedItems.length; i++) {
|
|
1635
|
+
const item = updatedItems[i];
|
|
1636
|
+
const context = {
|
|
1637
|
+
impersonatedAddress: isImpersonating ? impersonatedAddress : address,
|
|
1638
|
+
realAddress,
|
|
1639
|
+
component: item.component,
|
|
1640
|
+
observedValue: item.observedValue,
|
|
1641
|
+
onChainValue: item.onChainValue,
|
|
1642
|
+
indexedValue: item.indexedValue,
|
|
1643
|
+
dataSource: item.dataSource
|
|
1644
|
+
};
|
|
1645
|
+
try {
|
|
1646
|
+
const result = await validateLens(context, item.zone);
|
|
1647
|
+
if (result.valid) {
|
|
1648
|
+
updatedItems[i] = { ...item, status: "verified" };
|
|
1649
|
+
} else {
|
|
1650
|
+
updatedItems[i] = { ...item, status: "mismatch" };
|
|
1651
|
+
newIssues.push(...result.issues);
|
|
1652
|
+
result.issues.forEach((issue) => {
|
|
1653
|
+
addViolation({
|
|
1654
|
+
id: `${item.id}-${issue.type}`,
|
|
1655
|
+
timestamp: Date.now(),
|
|
1656
|
+
type: "behavioral",
|
|
1657
|
+
severity: issue.severity,
|
|
1658
|
+
message: issue.message,
|
|
1659
|
+
element: issue.component,
|
|
1660
|
+
suggestion: issue.suggestion
|
|
1661
|
+
});
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
} catch {
|
|
1665
|
+
updatedItems[i] = { ...item, status: "unknown" };
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
setItems(updatedItems);
|
|
1669
|
+
setQuestions(generateQuestions(newIssues));
|
|
1670
|
+
setIsQuerying(false);
|
|
1671
|
+
if (onDiagnosisComplete) {
|
|
1672
|
+
onDiagnosisComplete({
|
|
1673
|
+
verified: updatedItems.filter((i) => i.status === "verified"),
|
|
1674
|
+
issues: newIssues
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}, [address, items, isImpersonating, impersonatedAddress, realAddress, validateLens, addViolation, onDiagnosisComplete]);
|
|
1678
|
+
react.useEffect(() => {
|
|
1679
|
+
if (autoQuery && address && items.length > 0) {
|
|
1680
|
+
runDiagnostics();
|
|
1681
|
+
}
|
|
1682
|
+
}, [autoQuery, address]);
|
|
1683
|
+
const verifiedItems = items.filter((i) => i.status === "verified");
|
|
1684
|
+
const attentionItems = items.filter((i) => i.status === "mismatch" || i.status === "unknown");
|
|
1685
|
+
const pendingItems = items.filter((i) => i.status === "pending");
|
|
1686
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-panel", children: [
|
|
1687
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-header", children: [
|
|
1688
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Diagnostics" }),
|
|
1689
|
+
isImpersonating && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-diagnostic-lens-badge", children: [
|
|
1690
|
+
"Lens Active: ",
|
|
1691
|
+
impersonatedAddress?.slice(0, 6),
|
|
1692
|
+
"...",
|
|
1693
|
+
impersonatedAddress?.slice(-4)
|
|
1694
|
+
] })
|
|
1695
|
+
] }),
|
|
1696
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-diagnostic-error", children: error }),
|
|
1697
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-diagnostic-actions", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1698
|
+
"button",
|
|
1699
|
+
{
|
|
1700
|
+
onClick: runDiagnostics,
|
|
1701
|
+
disabled: isLoading || isQuerying || items.length === 0,
|
|
1702
|
+
className: "sigil-diagnostic-query-btn",
|
|
1703
|
+
children: isQuerying ? "Querying..." : "Run Diagnostics"
|
|
1704
|
+
}
|
|
1705
|
+
) }),
|
|
1706
|
+
verifiedItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-section sigil-diagnostic-verified", children: [
|
|
1707
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { children: [
|
|
1708
|
+
"\u2713 Verified (",
|
|
1709
|
+
verifiedItems.length,
|
|
1710
|
+
")"
|
|
1711
|
+
] }),
|
|
1712
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: verifiedItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-diagnostic-item verified", children: [
|
|
1713
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-label", children: item.label }),
|
|
1714
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-value", children: item.observedValue }),
|
|
1715
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-source", children: item.dataSource })
|
|
1716
|
+
] }, item.id)) })
|
|
1717
|
+
] }),
|
|
1718
|
+
attentionItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-section sigil-diagnostic-attention", children: [
|
|
1719
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { children: [
|
|
1720
|
+
"\u26A0 Needs Attention (",
|
|
1721
|
+
attentionItems.length,
|
|
1722
|
+
")"
|
|
1723
|
+
] }),
|
|
1724
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: attentionItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-diagnostic-item attention", children: [
|
|
1725
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-label", children: item.label }),
|
|
1726
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-comparison", children: [
|
|
1727
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-diagnostic-observed", children: [
|
|
1728
|
+
"UI: ",
|
|
1729
|
+
item.observedValue
|
|
1730
|
+
] }),
|
|
1731
|
+
item.onChainValue && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-diagnostic-onchain", children: [
|
|
1732
|
+
"On-chain: ",
|
|
1733
|
+
item.onChainValue
|
|
1734
|
+
] }),
|
|
1735
|
+
item.indexedValue && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-diagnostic-indexed", children: [
|
|
1736
|
+
"Indexed: ",
|
|
1737
|
+
item.indexedValue
|
|
1738
|
+
] })
|
|
1739
|
+
] })
|
|
1740
|
+
] }, item.id)) })
|
|
1741
|
+
] }),
|
|
1742
|
+
pendingItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-section sigil-diagnostic-pending", children: [
|
|
1743
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { children: [
|
|
1744
|
+
"\u25CB Pending (",
|
|
1745
|
+
pendingItems.length,
|
|
1746
|
+
")"
|
|
1747
|
+
] }),
|
|
1748
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: pendingItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-diagnostic-item pending", children: [
|
|
1749
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-label", children: item.label }),
|
|
1750
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-value", children: item.observedValue })
|
|
1751
|
+
] }, item.id)) })
|
|
1752
|
+
] }),
|
|
1753
|
+
questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-section sigil-diagnostic-questions", children: [
|
|
1754
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Questions for User" }),
|
|
1755
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: questions.map((q) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: `sigil-diagnostic-question ${q.severity}`, children: [
|
|
1756
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-question-text", children: q.question }),
|
|
1757
|
+
q.context && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-diagnostic-question-context", children: [
|
|
1758
|
+
"Suggestion: ",
|
|
1759
|
+
q.context
|
|
1760
|
+
] })
|
|
1761
|
+
] }, q.id)) })
|
|
1762
|
+
] }),
|
|
1763
|
+
diagnosticsState.violations.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-section sigil-diagnostic-violations", children: [
|
|
1764
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { children: [
|
|
1765
|
+
"Recent Violations (",
|
|
1766
|
+
diagnosticsState.violations.length,
|
|
1767
|
+
")"
|
|
1768
|
+
] }),
|
|
1769
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: diagnosticsState.violations.slice(0, 5).map((v) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: `sigil-diagnostic-violation ${v.severity}`, children: [
|
|
1770
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-diagnostic-violation-msg", children: v.message }),
|
|
1771
|
+
v.suggestion && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-diagnostic-violation-suggestion", children: [
|
|
1772
|
+
"\u2192 ",
|
|
1773
|
+
v.suggestion
|
|
1774
|
+
] })
|
|
1775
|
+
] }, v.id)) })
|
|
1776
|
+
] }),
|
|
1777
|
+
items.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-diagnostic-empty", children: [
|
|
1778
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "No items to diagnose." }),
|
|
1779
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Add diagnostic items to validate on-chain state." })
|
|
1780
|
+
] })
|
|
1781
|
+
] });
|
|
1782
|
+
}
|
|
1783
|
+
function useDiagnosticItems() {
|
|
1784
|
+
const [items, setItems] = react.useState([]);
|
|
1785
|
+
const addItem = react.useCallback((item) => {
|
|
1786
|
+
setItems((prev) => [
|
|
1787
|
+
...prev,
|
|
1788
|
+
{
|
|
1789
|
+
...item,
|
|
1790
|
+
id: `diag-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
1791
|
+
status: "pending"
|
|
1792
|
+
}
|
|
1793
|
+
]);
|
|
1794
|
+
}, []);
|
|
1795
|
+
const removeItem = react.useCallback((id) => {
|
|
1796
|
+
setItems((prev) => prev.filter((i) => i.id !== id));
|
|
1797
|
+
}, []);
|
|
1798
|
+
const clearItems = react.useCallback(() => {
|
|
1799
|
+
setItems([]);
|
|
1800
|
+
}, []);
|
|
1801
|
+
const updateItem = react.useCallback((id, updates) => {
|
|
1802
|
+
setItems(
|
|
1803
|
+
(prev) => prev.map((i) => i.id === id ? { ...i, ...updates } : i)
|
|
1804
|
+
);
|
|
1805
|
+
}, []);
|
|
1806
|
+
return {
|
|
1807
|
+
items,
|
|
1808
|
+
addItem,
|
|
1809
|
+
removeItem,
|
|
1810
|
+
clearItems,
|
|
1811
|
+
updateItem
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function formatEther(wei, decimals = 4) {
|
|
1815
|
+
const ethString = (Number(wei) / 1e18).toFixed(decimals);
|
|
1816
|
+
return `${ethString} ETH`;
|
|
1817
|
+
}
|
|
1818
|
+
function formatGas(gas) {
|
|
1819
|
+
if (gas >= 1000000n) {
|
|
1820
|
+
return `${(Number(gas) / 1e6).toFixed(2)}M`;
|
|
1821
|
+
}
|
|
1822
|
+
if (gas >= 1000n) {
|
|
1823
|
+
return `${(Number(gas) / 1e3).toFixed(1)}K`;
|
|
1824
|
+
}
|
|
1825
|
+
return gas.toString();
|
|
1826
|
+
}
|
|
1827
|
+
function truncateAddress2(address) {
|
|
1828
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1829
|
+
}
|
|
1830
|
+
function formatBalanceChange(change) {
|
|
1831
|
+
const sign = change.delta >= 0n ? "+" : "";
|
|
1832
|
+
return `${sign}${formatEther(change.delta)}`;
|
|
1833
|
+
}
|
|
1834
|
+
function estimateUSD(wei, ethPrice = 2e3) {
|
|
1835
|
+
const eth = Number(wei) / 1e18;
|
|
1836
|
+
const usd = eth * ethPrice;
|
|
1837
|
+
return `~$${usd.toFixed(2)}`;
|
|
1838
|
+
}
|
|
1839
|
+
function BalanceChangesSection({ changes }) {
|
|
1840
|
+
if (changes.length === 0)
|
|
1841
|
+
return null;
|
|
1842
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-section sigil-simulation-balances", children: [
|
|
1843
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Balance Changes" }),
|
|
1844
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: changes.map((change, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-simulation-balance-item", children: [
|
|
1845
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-address", title: change.address, children: truncateAddress2(change.address) }),
|
|
1846
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `sigil-simulation-delta ${change.delta >= 0n ? "positive" : "negative"}`, children: formatBalanceChange(change) }),
|
|
1847
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-token", children: change.symbol ?? (change.token ? truncateAddress2(change.token) : "ETH") })
|
|
1848
|
+
] }, i)) })
|
|
1849
|
+
] });
|
|
1850
|
+
}
|
|
1851
|
+
function EventLogsSection({ logs, showDecoded }) {
|
|
1852
|
+
if (logs.length === 0)
|
|
1853
|
+
return null;
|
|
1854
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-section sigil-simulation-logs", children: [
|
|
1855
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { children: [
|
|
1856
|
+
"Event Logs (",
|
|
1857
|
+
logs.length,
|
|
1858
|
+
")"
|
|
1859
|
+
] }),
|
|
1860
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ul", { children: [
|
|
1861
|
+
logs.slice(0, 10).map((log, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-simulation-log-item", children: [
|
|
1862
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-address", title: log.address, children: truncateAddress2(log.address) }),
|
|
1863
|
+
showDecoded && log.eventName ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-event-name", children: log.eventName }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-simulation-topic", title: log.topics[0], children: [
|
|
1864
|
+
log.topics[0]?.slice(0, 10),
|
|
1865
|
+
"..."
|
|
1866
|
+
] })
|
|
1867
|
+
] }, i)),
|
|
1868
|
+
logs.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "sigil-simulation-more", children: [
|
|
1869
|
+
"+",
|
|
1870
|
+
logs.length - 10,
|
|
1871
|
+
" more events"
|
|
1872
|
+
] })
|
|
1873
|
+
] })
|
|
1874
|
+
] });
|
|
1875
|
+
}
|
|
1876
|
+
function GasSummarySection({
|
|
1877
|
+
result,
|
|
1878
|
+
ethPrice
|
|
1879
|
+
}) {
|
|
1880
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-section sigil-simulation-gas", children: [
|
|
1881
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Gas" }),
|
|
1882
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-gas-grid", children: [
|
|
1883
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-gas-item", children: [
|
|
1884
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-label", children: "Used" }),
|
|
1885
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-value", children: formatGas(result.gasUsed) })
|
|
1886
|
+
] }),
|
|
1887
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-gas-item", children: [
|
|
1888
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-label", children: "Limit" }),
|
|
1889
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-value", children: formatGas(result.gasLimit) })
|
|
1890
|
+
] }),
|
|
1891
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-gas-item", children: [
|
|
1892
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-label", children: "Price" }),
|
|
1893
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-simulation-value", children: [
|
|
1894
|
+
(Number(result.effectiveGasPrice) / 1e9).toFixed(2),
|
|
1895
|
+
" gwei"
|
|
1896
|
+
] })
|
|
1897
|
+
] }),
|
|
1898
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-gas-item sigil-simulation-gas-total", children: [
|
|
1899
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-label", children: "Total Cost" }),
|
|
1900
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-simulation-value", children: [
|
|
1901
|
+
formatEther(result.totalCost),
|
|
1902
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-usd", children: estimateUSD(result.totalCost, ethPrice) })
|
|
1903
|
+
] })
|
|
1904
|
+
] })
|
|
1905
|
+
] })
|
|
1906
|
+
] });
|
|
1907
|
+
}
|
|
1908
|
+
function SimulationResultDisplay({
|
|
1909
|
+
result,
|
|
1910
|
+
ethPrice,
|
|
1911
|
+
showDecodedLogs
|
|
1912
|
+
}) {
|
|
1913
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `sigil-simulation-result ${result.success ? "success" : "failure"}`, children: [
|
|
1914
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-status", children: [
|
|
1915
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-status-icon", children: result.success ? "\u2713" : "\u2717" }),
|
|
1916
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-status-text", children: result.success ? "Transaction Successful" : "Transaction Failed" })
|
|
1917
|
+
] }),
|
|
1918
|
+
!result.success && result.revertReason && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-revert", children: [
|
|
1919
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Revert Reason" }),
|
|
1920
|
+
/* @__PURE__ */ jsxRuntime.jsx("pre", { className: "sigil-simulation-revert-message", children: result.revertReason })
|
|
1921
|
+
] }),
|
|
1922
|
+
/* @__PURE__ */ jsxRuntime.jsx(GasSummarySection, { result, ethPrice }),
|
|
1923
|
+
/* @__PURE__ */ jsxRuntime.jsx(BalanceChangesSection, { changes: result.balanceChanges }),
|
|
1924
|
+
/* @__PURE__ */ jsxRuntime.jsx(EventLogsSection, { logs: result.logs, showDecoded: showDecodedLogs }),
|
|
1925
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-section sigil-simulation-meta", children: [
|
|
1926
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-simulation-block", children: [
|
|
1927
|
+
"Block: ",
|
|
1928
|
+
result.blockNumber.toString()
|
|
1929
|
+
] }),
|
|
1930
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-time", children: new Date(result.timestamp).toLocaleTimeString() })
|
|
1931
|
+
] })
|
|
1932
|
+
] });
|
|
1933
|
+
}
|
|
1934
|
+
function TransactionInputForm({
|
|
1935
|
+
onSimulate,
|
|
1936
|
+
isSimulating
|
|
1937
|
+
}) {
|
|
1938
|
+
const [from, setFrom] = react.useState("");
|
|
1939
|
+
const [to, setTo] = react.useState("");
|
|
1940
|
+
const [value, setValue] = react.useState("");
|
|
1941
|
+
const [data, setData] = react.useState("");
|
|
1942
|
+
const handleSubmit = react.useCallback(
|
|
1943
|
+
(e) => {
|
|
1944
|
+
e.preventDefault();
|
|
1945
|
+
if (!from || !to)
|
|
1946
|
+
return;
|
|
1947
|
+
const tx = {
|
|
1948
|
+
from,
|
|
1949
|
+
to,
|
|
1950
|
+
value: value ? BigInt(value) : void 0,
|
|
1951
|
+
data: data ? data : void 0
|
|
1952
|
+
};
|
|
1953
|
+
onSimulate(tx);
|
|
1954
|
+
},
|
|
1955
|
+
[from, to, value, data, onSimulate]
|
|
1956
|
+
);
|
|
1957
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "sigil-simulation-form", children: [
|
|
1958
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-field", children: [
|
|
1959
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "sim-from", children: "From" }),
|
|
1960
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1961
|
+
"input",
|
|
1962
|
+
{
|
|
1963
|
+
id: "sim-from",
|
|
1964
|
+
type: "text",
|
|
1965
|
+
value: from,
|
|
1966
|
+
onChange: (e) => setFrom(e.target.value),
|
|
1967
|
+
placeholder: "0x...",
|
|
1968
|
+
disabled: isSimulating
|
|
1969
|
+
}
|
|
1970
|
+
)
|
|
1971
|
+
] }),
|
|
1972
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-field", children: [
|
|
1973
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "sim-to", children: "To" }),
|
|
1974
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1975
|
+
"input",
|
|
1976
|
+
{
|
|
1977
|
+
id: "sim-to",
|
|
1978
|
+
type: "text",
|
|
1979
|
+
value: to,
|
|
1980
|
+
onChange: (e) => setTo(e.target.value),
|
|
1981
|
+
placeholder: "0x...",
|
|
1982
|
+
disabled: isSimulating
|
|
1983
|
+
}
|
|
1984
|
+
)
|
|
1985
|
+
] }),
|
|
1986
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-field", children: [
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "sim-value", children: "Value (wei)" }),
|
|
1988
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1989
|
+
"input",
|
|
1990
|
+
{
|
|
1991
|
+
id: "sim-value",
|
|
1992
|
+
type: "text",
|
|
1993
|
+
value,
|
|
1994
|
+
onChange: (e) => setValue(e.target.value),
|
|
1995
|
+
placeholder: "0",
|
|
1996
|
+
disabled: isSimulating
|
|
1997
|
+
}
|
|
1998
|
+
)
|
|
1999
|
+
] }),
|
|
2000
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-field", children: [
|
|
2001
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "sim-data", children: "Data" }),
|
|
2002
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2003
|
+
"input",
|
|
2004
|
+
{
|
|
2005
|
+
id: "sim-data",
|
|
2006
|
+
type: "text",
|
|
2007
|
+
value: data,
|
|
2008
|
+
onChange: (e) => setData(e.target.value),
|
|
2009
|
+
placeholder: "0x...",
|
|
2010
|
+
disabled: isSimulating
|
|
2011
|
+
}
|
|
2012
|
+
)
|
|
2013
|
+
] }),
|
|
2014
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2015
|
+
"button",
|
|
2016
|
+
{
|
|
2017
|
+
type: "submit",
|
|
2018
|
+
disabled: isSimulating || !from || !to,
|
|
2019
|
+
className: "sigil-simulation-submit",
|
|
2020
|
+
children: isSimulating ? "Simulating..." : "Simulate"
|
|
2021
|
+
}
|
|
2022
|
+
)
|
|
2023
|
+
] });
|
|
2024
|
+
}
|
|
2025
|
+
function SimulationPanel({
|
|
2026
|
+
onSimulate,
|
|
2027
|
+
result,
|
|
2028
|
+
isSimulating = false,
|
|
2029
|
+
error,
|
|
2030
|
+
ethPrice = 2e3,
|
|
2031
|
+
showDecodedLogs = true
|
|
2032
|
+
}) {
|
|
2033
|
+
const handleSimulate = react.useCallback(
|
|
2034
|
+
async (tx) => {
|
|
2035
|
+
if (onSimulate) {
|
|
2036
|
+
await onSimulate(tx);
|
|
2037
|
+
}
|
|
2038
|
+
},
|
|
2039
|
+
[onSimulate]
|
|
2040
|
+
);
|
|
2041
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-panel", children: [
|
|
2042
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-simulation-header", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Transaction Simulation" }) }),
|
|
2043
|
+
onSimulate && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2044
|
+
TransactionInputForm,
|
|
2045
|
+
{
|
|
2046
|
+
onSimulate: handleSimulate,
|
|
2047
|
+
isSimulating
|
|
2048
|
+
}
|
|
2049
|
+
),
|
|
2050
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-simulation-error", children: error }),
|
|
2051
|
+
isSimulating && !result && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-loading", children: [
|
|
2052
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-simulation-spinner" }),
|
|
2053
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Running simulation..." })
|
|
2054
|
+
] }),
|
|
2055
|
+
result && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2056
|
+
SimulationResultDisplay,
|
|
2057
|
+
{
|
|
2058
|
+
result,
|
|
2059
|
+
ethPrice,
|
|
2060
|
+
showDecodedLogs
|
|
2061
|
+
}
|
|
2062
|
+
),
|
|
2063
|
+
!result && !isSimulating && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-simulation-empty", children: [
|
|
2064
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "No simulation results yet." }),
|
|
2065
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Enter transaction details above to simulate." })
|
|
2066
|
+
] })
|
|
2067
|
+
] });
|
|
2068
|
+
}
|
|
2069
|
+
function formatValue(value) {
|
|
2070
|
+
if (value === void 0)
|
|
2071
|
+
return "undefined";
|
|
2072
|
+
if (value === null)
|
|
2073
|
+
return "null";
|
|
2074
|
+
if (typeof value === "bigint")
|
|
2075
|
+
return `${value.toString()}n`;
|
|
2076
|
+
if (typeof value === "object") {
|
|
2077
|
+
try {
|
|
2078
|
+
return JSON.stringify(value, null, 2);
|
|
2079
|
+
} catch {
|
|
2080
|
+
return "[Object]";
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return String(value);
|
|
2084
|
+
}
|
|
2085
|
+
function valuesEqual(a, b) {
|
|
2086
|
+
if (a === b)
|
|
2087
|
+
return true;
|
|
2088
|
+
if (typeof a === "bigint" && typeof b === "bigint")
|
|
2089
|
+
return a === b;
|
|
2090
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
2091
|
+
try {
|
|
2092
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2093
|
+
} catch {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
function getSourceColor(source) {
|
|
2100
|
+
switch (source) {
|
|
2101
|
+
case "on-chain":
|
|
2102
|
+
return "sigil-source-onchain";
|
|
2103
|
+
case "indexed":
|
|
2104
|
+
return "sigil-source-indexed";
|
|
2105
|
+
case "cache":
|
|
2106
|
+
return "sigil-source-cache";
|
|
2107
|
+
case "local":
|
|
2108
|
+
return "sigil-source-local";
|
|
2109
|
+
default:
|
|
2110
|
+
return "sigil-source-unknown";
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function getChangeColor(type) {
|
|
2114
|
+
switch (type) {
|
|
2115
|
+
case "added":
|
|
2116
|
+
return "sigil-change-added";
|
|
2117
|
+
case "removed":
|
|
2118
|
+
return "sigil-change-removed";
|
|
2119
|
+
case "modified":
|
|
2120
|
+
return "sigil-change-modified";
|
|
2121
|
+
default:
|
|
2122
|
+
return "sigil-change-unchanged";
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
function ComparisonRow({ item }) {
|
|
2126
|
+
const [expanded, setExpanded] = react.useState(false);
|
|
2127
|
+
const leftValue = item.left ? formatValue(item.left.value) : "\u2014";
|
|
2128
|
+
const rightValue = item.right ? formatValue(item.right.value) : "\u2014";
|
|
2129
|
+
const isLongValue = leftValue.length > 50 || rightValue.length > 50;
|
|
2130
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `sigil-comparison-row ${item.isDifferent ? "different" : ""}`, children: [
|
|
2131
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-path", children: [
|
|
2132
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: item.path }),
|
|
2133
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `sigil-comparison-badge ${getChangeColor(item.changeType)}`, children: item.changeType })
|
|
2134
|
+
] }),
|
|
2135
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-values", children: [
|
|
2136
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-comparison-value sigil-comparison-left", children: item.left ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2137
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `sigil-comparison-source ${getSourceColor(item.left.source)}`, children: item.left.source }),
|
|
2138
|
+
isLongValue ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2139
|
+
"button",
|
|
2140
|
+
{
|
|
2141
|
+
className: "sigil-comparison-expand",
|
|
2142
|
+
onClick: () => setExpanded(!expanded),
|
|
2143
|
+
children: expanded ? "Collapse" : "Expand"
|
|
2144
|
+
}
|
|
2145
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("code", { className: "sigil-comparison-value-text", children: leftValue })
|
|
2146
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-comparison-empty", children: "\u2014" }) }),
|
|
2147
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-comparison-value sigil-comparison-right", children: item.right ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2148
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `sigil-comparison-source ${getSourceColor(item.right.source)}`, children: item.right.source }),
|
|
2149
|
+
isLongValue ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2150
|
+
"button",
|
|
2151
|
+
{
|
|
2152
|
+
className: "sigil-comparison-expand",
|
|
2153
|
+
onClick: () => setExpanded(!expanded),
|
|
2154
|
+
children: expanded ? "Collapse" : "Expand"
|
|
2155
|
+
}
|
|
2156
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("code", { className: "sigil-comparison-value-text", children: rightValue })
|
|
2157
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-comparison-empty", children: "\u2014" }) })
|
|
2158
|
+
] }),
|
|
2159
|
+
expanded && isLongValue && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-expanded", children: [
|
|
2160
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-comparison-expanded-left", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { children: leftValue }) }),
|
|
2161
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-comparison-expanded-right", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { children: rightValue }) })
|
|
2162
|
+
] })
|
|
2163
|
+
] });
|
|
2164
|
+
}
|
|
2165
|
+
function StateComparison({
|
|
2166
|
+
leftSnapshot,
|
|
2167
|
+
rightSnapshot,
|
|
2168
|
+
leftLabel = "Before",
|
|
2169
|
+
rightLabel = "After",
|
|
2170
|
+
showOnlyDifferences = false,
|
|
2171
|
+
filterSource = null,
|
|
2172
|
+
onExport
|
|
2173
|
+
}) {
|
|
2174
|
+
const [filter, setFilter] = react.useState("");
|
|
2175
|
+
const [onlyDiff, setOnlyDiff] = react.useState(showOnlyDifferences);
|
|
2176
|
+
const [sourceFilter, setSourceFilter] = react.useState(filterSource);
|
|
2177
|
+
const comparisonItems = react.useMemo(() => {
|
|
2178
|
+
const items = [];
|
|
2179
|
+
const allPaths = /* @__PURE__ */ new Set();
|
|
2180
|
+
if (leftSnapshot) {
|
|
2181
|
+
Object.keys(leftSnapshot.values).forEach((path) => allPaths.add(path));
|
|
2182
|
+
}
|
|
2183
|
+
if (rightSnapshot) {
|
|
2184
|
+
Object.keys(rightSnapshot.values).forEach((path) => allPaths.add(path));
|
|
2185
|
+
}
|
|
2186
|
+
for (const path of allPaths) {
|
|
2187
|
+
const left = leftSnapshot?.values[path];
|
|
2188
|
+
const right = rightSnapshot?.values[path];
|
|
2189
|
+
let changeType = "unchanged";
|
|
2190
|
+
let isDifferent = false;
|
|
2191
|
+
if (left && !right) {
|
|
2192
|
+
changeType = "removed";
|
|
2193
|
+
isDifferent = true;
|
|
2194
|
+
} else if (!left && right) {
|
|
2195
|
+
changeType = "added";
|
|
2196
|
+
isDifferent = true;
|
|
2197
|
+
} else if (left && right && !valuesEqual(left.value, right.value)) {
|
|
2198
|
+
changeType = "modified";
|
|
2199
|
+
isDifferent = true;
|
|
2200
|
+
}
|
|
2201
|
+
items.push({
|
|
2202
|
+
path,
|
|
2203
|
+
left,
|
|
2204
|
+
right,
|
|
2205
|
+
isDifferent,
|
|
2206
|
+
changeType
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
items.sort((a, b) => a.path.localeCompare(b.path));
|
|
2210
|
+
return items;
|
|
2211
|
+
}, [leftSnapshot, rightSnapshot]);
|
|
2212
|
+
const filteredItems = react.useMemo(() => {
|
|
2213
|
+
return comparisonItems.filter((item) => {
|
|
2214
|
+
if (filter && !item.path.toLowerCase().includes(filter.toLowerCase())) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
if (onlyDiff && !item.isDifferent) {
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
if (sourceFilter) {
|
|
2221
|
+
const leftMatch = item.left?.source === sourceFilter;
|
|
2222
|
+
const rightMatch = item.right?.source === sourceFilter;
|
|
2223
|
+
if (!leftMatch && !rightMatch) {
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return true;
|
|
2228
|
+
});
|
|
2229
|
+
}, [comparisonItems, filter, onlyDiff, sourceFilter]);
|
|
2230
|
+
const stats = react.useMemo(() => {
|
|
2231
|
+
const added = comparisonItems.filter((i) => i.changeType === "added").length;
|
|
2232
|
+
const removed = comparisonItems.filter((i) => i.changeType === "removed").length;
|
|
2233
|
+
const modified = comparisonItems.filter((i) => i.changeType === "modified").length;
|
|
2234
|
+
const unchanged = comparisonItems.filter((i) => i.changeType === "unchanged").length;
|
|
2235
|
+
return { added, removed, modified, unchanged, total: comparisonItems.length };
|
|
2236
|
+
}, [comparisonItems]);
|
|
2237
|
+
const handleExport = react.useCallback(() => {
|
|
2238
|
+
if (onExport) {
|
|
2239
|
+
onExport(filteredItems);
|
|
2240
|
+
} else {
|
|
2241
|
+
const blob = new Blob([JSON.stringify(filteredItems, null, 2)], {
|
|
2242
|
+
type: "application/json"
|
|
2243
|
+
});
|
|
2244
|
+
const url = URL.createObjectURL(blob);
|
|
2245
|
+
const a = document.createElement("a");
|
|
2246
|
+
a.href = url;
|
|
2247
|
+
a.download = `state-comparison-${Date.now()}.json`;
|
|
2248
|
+
a.click();
|
|
2249
|
+
URL.revokeObjectURL(url);
|
|
2250
|
+
}
|
|
2251
|
+
}, [filteredItems, onExport]);
|
|
2252
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-state-comparison", children: [
|
|
2253
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-header", children: [
|
|
2254
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: "State Comparison" }),
|
|
2255
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-stats", children: [
|
|
2256
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-stat sigil-stat-added", children: [
|
|
2257
|
+
"+",
|
|
2258
|
+
stats.added
|
|
2259
|
+
] }),
|
|
2260
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-stat sigil-stat-removed", children: [
|
|
2261
|
+
"-",
|
|
2262
|
+
stats.removed
|
|
2263
|
+
] }),
|
|
2264
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-stat sigil-stat-modified", children: [
|
|
2265
|
+
"~",
|
|
2266
|
+
stats.modified
|
|
2267
|
+
] }),
|
|
2268
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sigil-stat sigil-stat-unchanged", children: [
|
|
2269
|
+
"=",
|
|
2270
|
+
stats.unchanged
|
|
2271
|
+
] })
|
|
2272
|
+
] })
|
|
2273
|
+
] }),
|
|
2274
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-snapshots", children: [
|
|
2275
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-snapshot-left", children: [
|
|
2276
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-comparison-label", children: leftLabel }),
|
|
2277
|
+
leftSnapshot && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-comparison-time", children: new Date(leftSnapshot.timestamp).toLocaleTimeString() })
|
|
2278
|
+
] }),
|
|
2279
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-snapshot-right", children: [
|
|
2280
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-comparison-label", children: rightLabel }),
|
|
2281
|
+
rightSnapshot && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sigil-comparison-time", children: new Date(rightSnapshot.timestamp).toLocaleTimeString() })
|
|
2282
|
+
] })
|
|
2283
|
+
] }),
|
|
2284
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sigil-comparison-filters", children: [
|
|
2285
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2286
|
+
"input",
|
|
2287
|
+
{
|
|
2288
|
+
type: "text",
|
|
2289
|
+
value: filter,
|
|
2290
|
+
onChange: (e) => setFilter(e.target.value),
|
|
2291
|
+
placeholder: "Filter by path...",
|
|
2292
|
+
className: "sigil-comparison-filter-input"
|
|
2293
|
+
}
|
|
2294
|
+
),
|
|
2295
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "sigil-comparison-filter-checkbox", children: [
|
|
2296
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2297
|
+
"input",
|
|
2298
|
+
{
|
|
2299
|
+
type: "checkbox",
|
|
2300
|
+
checked: onlyDiff,
|
|
2301
|
+
onChange: (e) => setOnlyDiff(e.target.checked)
|
|
2302
|
+
}
|
|
2303
|
+
),
|
|
2304
|
+
"Only differences"
|
|
2305
|
+
] }),
|
|
2306
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2307
|
+
"select",
|
|
2308
|
+
{
|
|
2309
|
+
value: sourceFilter ?? "",
|
|
2310
|
+
onChange: (e) => setSourceFilter(e.target.value || null),
|
|
2311
|
+
className: "sigil-comparison-filter-select",
|
|
2312
|
+
children: [
|
|
2313
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All sources" }),
|
|
2314
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "on-chain", children: "On-chain" }),
|
|
2315
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "indexed", children: "Indexed" }),
|
|
2316
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "cache", children: "Cache" }),
|
|
2317
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "local", children: "Local" })
|
|
2318
|
+
]
|
|
2319
|
+
}
|
|
2320
|
+
),
|
|
2321
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleExport, className: "sigil-comparison-export", children: "Export JSON" })
|
|
2322
|
+
] }),
|
|
2323
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-comparison-items", children: filteredItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sigil-comparison-empty-state", children: comparisonItems.length === 0 ? "No state to compare. Capture snapshots first." : "No items match the current filters." }) : filteredItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(ComparisonRow, { item }, item.path)) })
|
|
2324
|
+
] });
|
|
2325
|
+
}
|
|
2326
|
+
function useStateSnapshots() {
|
|
2327
|
+
const [snapshots, setSnapshots] = react.useState([]);
|
|
2328
|
+
const [leftId, setLeftId] = react.useState(null);
|
|
2329
|
+
const [rightId, setRightId] = react.useState(null);
|
|
2330
|
+
const captureSnapshot = react.useCallback((label, values) => {
|
|
2331
|
+
const snapshot = {
|
|
2332
|
+
id: `snap-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
2333
|
+
label,
|
|
2334
|
+
timestamp: Date.now(),
|
|
2335
|
+
values
|
|
2336
|
+
};
|
|
2337
|
+
setSnapshots((prev) => [...prev, snapshot]);
|
|
2338
|
+
return snapshot;
|
|
2339
|
+
}, []);
|
|
2340
|
+
const deleteSnapshot = react.useCallback((id) => {
|
|
2341
|
+
setSnapshots((prev) => prev.filter((s) => s.id !== id));
|
|
2342
|
+
if (leftId === id)
|
|
2343
|
+
setLeftId(null);
|
|
2344
|
+
if (rightId === id)
|
|
2345
|
+
setRightId(null);
|
|
2346
|
+
}, [leftId, rightId]);
|
|
2347
|
+
const clearSnapshots = react.useCallback(() => {
|
|
2348
|
+
setSnapshots([]);
|
|
2349
|
+
setLeftId(null);
|
|
2350
|
+
setRightId(null);
|
|
2351
|
+
}, []);
|
|
2352
|
+
const leftSnapshot = snapshots.find((s) => s.id === leftId) ?? null;
|
|
2353
|
+
const rightSnapshot = snapshots.find((s) => s.id === rightId) ?? null;
|
|
2354
|
+
return {
|
|
2355
|
+
snapshots,
|
|
2356
|
+
leftSnapshot,
|
|
2357
|
+
rightSnapshot,
|
|
2358
|
+
leftId,
|
|
2359
|
+
rightId,
|
|
2360
|
+
setLeftId,
|
|
2361
|
+
setRightId,
|
|
2362
|
+
captureSnapshot,
|
|
2363
|
+
deleteSnapshot,
|
|
2364
|
+
clearSnapshots
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
exports.DevToolbar = DevToolbar;
|
|
2369
|
+
exports.DevToolbarProvider = DevToolbarProvider;
|
|
2370
|
+
exports.DevToolbarTrigger = DevToolbarTrigger;
|
|
2371
|
+
exports.DevToolbarWithTrigger = DevToolbarWithTrigger;
|
|
2372
|
+
exports.DiagnosticPanel = DiagnosticPanel;
|
|
2373
|
+
exports.IPCClient = IPCClient;
|
|
2374
|
+
exports.LensActiveBadge = LensActiveBadge;
|
|
2375
|
+
exports.LocalStorageTransport = LocalStorageTransport;
|
|
2376
|
+
exports.MockTransport = MockTransport;
|
|
2377
|
+
exports.SimulationPanel = SimulationPanel;
|
|
2378
|
+
exports.StateComparison = StateComparison;
|
|
2379
|
+
exports.UserLens = UserLens;
|
|
2380
|
+
exports.createAnvilForkService = createAnvilForkService;
|
|
2381
|
+
exports.createForkService = createForkService;
|
|
2382
|
+
exports.createSimulationService = createSimulationService;
|
|
2383
|
+
exports.createTenderlyForkService = createTenderlyForkService;
|
|
2384
|
+
exports.getForkService = getForkService;
|
|
2385
|
+
exports.getIPCClient = getIPCClient;
|
|
2386
|
+
exports.getSimulationService = getSimulationService;
|
|
2387
|
+
exports.resetForkService = resetForkService;
|
|
2388
|
+
exports.resetIPCClient = resetIPCClient;
|
|
2389
|
+
exports.resetSimulationService = resetSimulationService;
|
|
2390
|
+
exports.useDevToolbar = useDevToolbar;
|
|
2391
|
+
exports.useDevToolbarConfig = useDevToolbarConfig;
|
|
2392
|
+
exports.useDevToolbarSelector = useDevToolbarSelector;
|
|
2393
|
+
exports.useDiagnosticItems = useDiagnosticItems;
|
|
2394
|
+
exports.useForkState = useForkState;
|
|
2395
|
+
exports.useIPCClient = useIPCClient;
|
|
2396
|
+
exports.useImpersonatedAddress = useImpersonatedAddress;
|
|
2397
|
+
exports.useIsImpersonating = useIsImpersonating;
|
|
2398
|
+
exports.useLensAwareAccount = useLensAwareAccount;
|
|
2399
|
+
exports.useSavedAddresses = useSavedAddresses;
|
|
2400
|
+
exports.useSimulation = useSimulation;
|
|
2401
|
+
exports.useStateSnapshots = useStateSnapshots;
|
|
2402
|
+
exports.useTransactionSimulation = useTransactionSimulation;
|
|
2403
|
+
//# sourceMappingURL=out.js.map
|
|
2404
|
+
//# sourceMappingURL=index.cjs.map
|