Skip to content

Commit aeb210f

Browse files
authored
feat(explorer): abi explorer (#3635)
1 parent b18c0ef commit aeb210f

9 files changed

Lines changed: 215 additions & 1065 deletions

File tree

.changeset/thin-nails-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/explorer": patch
3+
---
4+
5+
Added an ABI page for exploring world ABI. The ABI Explorer also includes a form for searching custom errors or functions based on their selectors.

packages/explorer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"react": "^18",
7474
"react-dom": "^18",
7575
"react-hook-form": "^7.52.1",
76+
"react18-json-view": "^0.2.9",
7677
"sonner": "^1.5.0",
7778
"sql-autocomplete": "^1.1.1",
7879
"tailwind-merge": "^1.12.0",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import JsonView from "react18-json-view";
4+
import { CopyButton } from "../../../../../../components/CopyButton";
5+
import { Skeleton } from "../../../../../../components/ui/Skeleton";
6+
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";
7+
8+
export function AbiExplorer() {
9+
const { data, isLoading, isError } = useWorldAbiQuery();
10+
11+
if (isLoading) {
12+
return <Skeleton className="h-[106px] w-full" />;
13+
} else if (isError) {
14+
throw new Error("Failed to fetch ABI");
15+
}
16+
17+
return (
18+
<div className="space-y-4">
19+
<h4 className="font-semibold uppercase">ABI</h4>
20+
<pre className="text-md relative mb-4 rounded border border-white/20 p-3 text-sm">
21+
<JsonView src={data?.abi} theme="a11y" />
22+
<CopyButton value={JSON.stringify(data?.abi, null, 2)} className="absolute right-1.5 top-1.5" />
23+
</pre>
24+
</div>
25+
);
26+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"use client";
2+
3+
import { AbiEvent, AbiFunction, toFunctionSelector } from "viem";
4+
import { formatAbiItem } from "viem/utils";
5+
import * as z from "zod";
6+
import { useState } from "react";
7+
import "react18-json-view/src/dark.css";
8+
import "react18-json-view/src/style.css";
9+
import { useForm } from "react-hook-form";
10+
import { zodResolver } from "@hookform/resolvers/zod";
11+
import { CopyButton } from "../../../../../../components/CopyButton";
12+
import { Button } from "../../../../../../components/ui/Button";
13+
import {
14+
Form,
15+
FormControl,
16+
FormDescription,
17+
FormField,
18+
FormItem,
19+
FormLabel,
20+
FormMessage,
21+
} from "../../../../../../components/ui/Form";
22+
import { Input } from "../../../../../../components/ui/Input";
23+
import { Skeleton } from "../../../../../../components/ui/Skeleton";
24+
import { cn } from "../../../../../../utils";
25+
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";
26+
import { getErrorSelector } from "./getErrorSelector";
27+
28+
const formSchema = z.object({
29+
selector: z.string().min(1).optional(),
30+
});
31+
32+
export function DecodeForm() {
33+
const { data, isLoading } = useWorldAbiQuery();
34+
const form = useForm<z.infer<typeof formSchema>>({
35+
resolver: zodResolver(formSchema),
36+
});
37+
const [result, setResult] = useState<AbiFunction | AbiEvent>();
38+
39+
function onSubmit({ selector }: z.infer<typeof formSchema>) {
40+
const items = data?.abi.filter((item) => item.type === "function" || item.type === "error");
41+
const abiItem = items?.find((item) => {
42+
if (item.type === "function") {
43+
return toFunctionSelector(item) === selector;
44+
} else if (item.type === "error") {
45+
return getErrorSelector(item) === selector;
46+
}
47+
48+
return false;
49+
});
50+
51+
setResult(abiItem);
52+
}
53+
54+
if (isLoading) {
55+
return <Skeleton className="h-[152px] w-full" />;
56+
}
57+
58+
return (
59+
<Form {...form}>
60+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
61+
<FormField
62+
control={form.control}
63+
name="selector"
64+
render={({ field }) => (
65+
<FormItem>
66+
<FormLabel>Encoded selector</FormLabel>
67+
<FormControl>
68+
<Input placeholder="0xf0f0f0f0" type="text" {...field} />
69+
</FormControl>
70+
<FormDescription>Find the function or error by its selector</FormDescription>
71+
<FormMessage />
72+
</FormItem>
73+
)}
74+
/>
75+
76+
{form.formState.isSubmitted && (
77+
<pre
78+
className={cn("text-md relative mt-4 rounded border border-white/20 p-3 text-sm", {
79+
"border-red-400 bg-red-100": !result,
80+
})}
81+
>
82+
{result ? (
83+
<>
84+
<span className="mr-2 text-sm opacity-50">{result.type === "function" ? "function" : "error"}</span>
85+
<span>{formatAbiItem(result)}</span>
86+
<CopyButton value={JSON.stringify(result, null, 2)} className="absolute right-1.5 top-1.5" />
87+
</>
88+
) : (
89+
<span className="text-red-700">No matching function or error found for this selector</span>
90+
)}
91+
</pre>
92+
)}
93+
94+
<Button type="submit" size="sm">
95+
Find
96+
</Button>
97+
</form>
98+
</Form>
99+
);
100+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { AbiItem } from "viem";
2+
import { keccak256, stringToHex } from "viem";
3+
4+
export function getErrorSelector(errorAbi: AbiItem) {
5+
if (errorAbi.type !== "error") {
6+
throw new Error("Abi item is not an error");
7+
}
8+
9+
const inputTypes = errorAbi.inputs.map((input) => input.type);
10+
const signature = `${errorAbi.name}(${inputTypes.join(",")})`;
11+
const hash = keccak256(stringToHex(signature));
12+
13+
return hash.slice(0, 10);
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Metadata } from "next";
2+
import { Separator } from "../../../../../../components/ui/Separator";
3+
import { AbiExplorer } from "./AbiExplorer";
4+
import { DecodeForm } from "./DecodeForm";
5+
6+
export const metadata: Metadata = {
7+
title: "ABI Explorer",
8+
};
9+
10+
export default async function UtilsPage() {
11+
return (
12+
<div className="flex h-[calc(100vh-70px)] flex-col space-y-8">
13+
<DecodeForm />
14+
15+
<Separator />
16+
17+
<AbiExplorer />
18+
</div>
19+
);
20+
}

packages/explorer/src/app/(explorer)/globals.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ html,
66
body,
77
.container {
88
font-family: var(--font-jetbrains-mono);
9+
background: var(--color-background);
910
}
1011

1112
@layer base {

packages/explorer/src/components/Navigation.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function Navigation() {
3636
<NavigationLink href="explore">Explore</NavigationLink>
3737
<NavigationLink href="interact">Interact</NavigationLink>
3838
<NavigationLink href="observe">Observe</NavigationLink>
39+
<NavigationLink href="abi">ABI</NavigationLink>
3940
</div>
4041

4142
{isFetched && !data?.isWorldDeployed && (

0 commit comments

Comments
 (0)