@polareth/evmstate
Version:
A TypeScript library for tracing, and visualizing EVM state changes with detailed human-readable labeling.
273 lines (241 loc) • 8.9 kB
text/typescript
import type { MemoryClient } from "tevm";
import { createMemoryClient, createSyncStoragePersister, parseEther } from "tevm";
import type { ExtractAbiFunctions } from "abitype";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { createHighlighter, type Highlighter, type ThemeRegistrationAny } from "shiki";
import type { ContractFunctionArgs } from "viem";
import { traceState } from "@polareth/evmstate";
import { type CodeBlockRef } from "~/components/code-block/index.js";
import {
callerAddress,
contract,
functionDescriptions,
layout,
localStorageKey,
} from "~/components/playground/constants.js";
import { usePlaygroundStore } from "~/components/stores/trace-store.js";
import themeLight from "~/themes/theme-light.json" with { type: "json" };
export const usePlayground = () => {
const [client, setClient] = useState<MemoryClient | undefined>(undefined);
const { traces, addTrace, clearTraces } = usePlaygroundStore();
const [highlighter, setHighlighter] = useState<Highlighter | undefined>(undefined);
useEffect(() => {
const initHighlighter = async () => {
const highlighter = await createHighlighter({
themes: ["poimandres", themeLight as ThemeRegistrationAny],
langs: ["typescript"],
});
setHighlighter(highlighter);
};
initHighlighter();
}, []);
const [selectedFunction, setSelectedFunction] = useState<ExtractAbiFunctions<typeof contract.abi, "nonpayable">>(
contract.abi.find(
(item) => item.type === "function" && item.name === Object.keys(functionDescriptions)[0],
) as ExtractAbiFunctions<typeof contract.abi, "nonpayable">,
);
const [args, setArgs] = useState<
ContractFunctionArgs<typeof contract.abi, "nonpayable", typeof selectedFunction.name> | never[]
>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Ref to hold the array of CodeBlock refs for traces
const traceCodeBlockRefs = useRef<Array<CodeBlockRef | null>>([]);
// Update refs array when traces change
useEffect(() => {
// Resize the refs array to match the traces array
traceCodeBlockRefs.current = traceCodeBlockRefs.current.slice(0, traces.length);
while (traceCodeBlockRefs.current.length < traces.length) {
traceCodeBlockRefs.current.push(null);
}
}, [traces.length]);
const init = useCallback(async () => {
if (typeof window === "undefined") return;
setIsLoading(true);
// Init the client
const client = createMemoryClient({
persister: createSyncStoragePersister({
storage: localStorage,
key: localStorageKey,
}),
});
setClient(client);
// Get the account first to see if we initialized already in the past
const account = await client.tevmGetAccount({ address: callerAddress.toString(), throwOnFail: false });
if (account && account.nonce !== 0n) {
setIsLoading(false);
return;
}
try {
const { errors } = await client.tevmSetAccount({
address: callerAddress.toString(),
nonce: 0n,
balance: parseEther("100"),
});
if (errors) throw new Error(errors.map((e) => e.message).join("\n"));
} catch (err) {
console.error("Failed to initialize caller account:", err);
setError("Failed to initialize caller account");
}
try {
// Deploy the contract
const { txHash } = await client.tevmDeploy({
from: callerAddress.toString(),
abi: contract.abi,
bytecode: contract.bytecode,
args: [callerAddress.toString(), "Playground"],
addToBlockchain: true,
});
if (!txHash) throw new Error("txHash is undefined");
// Get a trace for deployment
const state = await traceState({
client,
txHash,
fetchContracts: false,
fetchStorageLayouts: false,
// something nice is that we can already provide the layout as we know the address it will be deployed to
storageLayouts: { [contract.address.toLowerCase()]: layout },
});
// Use the store's addTrace method instead
addTrace({
functionName: "deploy",
args: [callerAddress.toString(), "Playground"],
state,
});
} catch (err) {
console.error("Failed to deploy contract:", err);
setError("Failed to deploy contract");
}
setIsLoading(false);
}, [addTrace]);
// Init client, caller account and deploy contract on component mount
useEffect(() => {
init();
}, [contract, addTrace]);
// Handle function selection change
const handleFunctionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const funcName = e.target.value;
const func = contract.abi.find((f) => f.type === "function" && f.name === funcName) ?? undefined;
if (!func) throw new Error(`Function ${funcName} not found`);
setSelectedFunction(func as ExtractAbiFunctions<typeof contract.abi, "nonpayable">);
setArgs([]);
};
// Handle argument input change
const handleArgChange = (index: number, value: string) => {
const newArgs = [...(args ?? [])];
newArgs[index] = value;
// @ts-expect-error - not typed
setArgs(newArgs);
};
// Execute function
const executeFunction = async () => {
if (!client) return;
try {
setError(null);
setIsLoading(true);
// Get the state
const state = await traceState({
client,
from: callerAddress.toString(),
to: contract.address,
storageLayouts: { [contract.address.toLowerCase()]: layout },
fetchContracts: false,
fetchStorageLayouts: false,
abi: contract.abi,
functionName: selectedFunction.name,
// @ts-expect-error - args mismatch
args,
});
// Actually execute the function for follow-up
await client.tevmContract({
from: callerAddress.toString(),
to: contract.address,
abi: contract.abi,
functionName: selectedFunction.name,
// @ts-expect-error - args mismatch
args,
addToBlockchain: true,
});
// Add to traces history
const newTrace = {
functionName: selectedFunction.name,
args: [...args],
state,
};
addTrace(newTrace);
console.log(newTrace);
// Collapse all existing traces and expand the new one
traceCodeBlockRefs.current.forEach((ref) => ref?.collapse());
} catch (err) {
setError(`Error executing function: ${err instanceof Error ? err.message : JSON.stringify(err)}`);
} finally {
setIsLoading(false);
}
};
// Function to collapse all trace CodeBlocks
const handleCollapseAllTraces = () => {
traceCodeBlockRefs.current.forEach((ref) => {
if (ref) ref.collapse();
});
};
const handleReset = async () => {
if (typeof window === "undefined") return;
localStorage.removeItem(localStorageKey);
clearTraces(); // Use the store's clearTraces method
await init();
};
// Generate random values for the current function's arguments
const generateRandomArgs = useCallback(() => {
if (!selectedFunction.inputs) return;
let randomArgs = selectedFunction.inputs.map((input) => {
// Handle different types with appropriate random values
switch (input.type) {
case "address":
return `0x${Array.from({ length: 40 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
case "uint256":
case "uint8":
case "uint16":
case "uint32":
// Generate a reasonable random number (not too large)
return Math.floor(Math.random() * 1000);
case "bool":
return Math.random() > 0.5;
case "string":
// Generate a random string of 5-10 characters
const length = Math.floor(Math.random() * 6) + 5;
return Array.from({ length }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join("");
case "bytes":
// Generate random bytes of random length (2-10 bytes)
const bytesLength = Math.floor(Math.random() * 9) + 2;
return `0x${Array.from({ length: bytesLength * 2 }, () => Math.floor(Math.random() * 16).toString(16)).join(
"",
)}`;
default:
return "";
}
});
if (selectedFunction.name === "setFixedValue") {
randomArgs[0] = Math.floor(Math.random() * 3);
}
// @ts-expect-error - args mismatch
setArgs(randomArgs);
}, [selectedFunction]);
return {
client,
traces,
addTrace,
clearTraces,
traceCodeBlockRefs,
highlighter,
selectedFunction,
args,
isLoading,
error,
handleFunctionChange,
handleArgChange,
executeFunction,
handleCollapseAllTraces,
handleReset,
generateRandomArgs,
};
};