Skip to content

Commit 091ece6

Browse files
alvrskarooolis
andauthored
feat(explorer): decode function calls (#3710)
Co-authored-by: Karolis Ramanauskas <hello@karolisram.com>
1 parent 9c89cc3 commit 091ece6

2 files changed

Lines changed: 158 additions & 24 deletions

File tree

‎.changeset/breezy-wolves-notice.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+
The Worlds Explorer explorer now supports decoding function data.

‎packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/decode/DecodeForm.tsx‎

Lines changed: 153 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
"use client";
22

3-
import { AbiFunction, AbiItem, toFunctionSelector } from "viem";
3+
import {
4+
AbiFunction,
5+
AbiItem,
6+
Hex,
7+
decodeErrorResult,
8+
decodeFunctionData,
9+
parseAbiItem,
10+
toFunctionSelector,
11+
} from "viem";
412
import { formatAbiItem } from "viem/utils";
513
import * as z from "zod";
6-
import { useState } from "react";
14+
import { useMemo, useState } from "react";
715
import "react18-json-view/src/dark.css";
816
import "react18-json-view/src/style.css";
917
import { useForm } from "react-hook-form";
1018
import { zodResolver } from "@hookform/resolvers/zod";
19+
import { Resource, hexToResource, resourceToLabel } from "@latticexyz/common";
1120
import { CopyButton } from "../../../../../../components/CopyButton";
1221
import { Button } from "../../../../../../components/ui/Button";
1322
import {
@@ -27,9 +36,23 @@ import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";
2736
import { getErrorSelector } from "./getErrorSelector";
2837

2938
type AbiError = AbiItem & { type: "error" };
39+
type ResourceItem = { type: "resource"; id: string; resource: Resource };
40+
type SelectorDatabase = { type: "signature"; selectors: string[] };
41+
type Result = {
42+
type: string;
43+
label: string;
44+
decodedCall?: DecodedFunctionCall;
45+
};
46+
47+
type DecodedFunctionCall = {
48+
args: readonly unknown[];
49+
abi: AbiFunction | AbiError;
50+
functionName?: string;
51+
errorName?: string;
52+
};
3053

3154
const formSchema = z.object({
32-
selector: z.string().min(1).optional(),
55+
encodedData: z.string().min(1).optional(),
3356
});
3457

3558
function isAbiFunction(item: AbiItem): item is AbiFunction {
@@ -40,30 +63,124 @@ function isAbiError(item: AbiItem): item is AbiItem & { type: "error" } {
4063
return item.type === "error";
4164
}
4265

66+
function stringifyValue(value: unknown): string {
67+
if (value === null) return "null";
68+
if (value === undefined) return "undefined";
69+
if (typeof value === "object") {
70+
if (Array.isArray(value)) {
71+
return `[${value.map(stringifyValue).join(", ")}]`;
72+
}
73+
return `{${Object.entries(value as Record<string, unknown>)
74+
.map(([key, val]) => `${key}: ${stringifyValue(val)}`)
75+
.join(", ")}}`;
76+
}
77+
return String(value);
78+
}
79+
80+
function decodeAbiItem(abiItem: AbiFunction | AbiError, data: Hex): DecodedFunctionCall {
81+
if (abiItem.type === "function") {
82+
const result = decodeFunctionData({ abi: [abiItem], data });
83+
return { args: result.args, abi: abiItem, functionName: result.functionName };
84+
} else {
85+
const result = decodeErrorResult({ abi: [abiItem], data });
86+
return { args: result.args, abi: abiItem, errorName: result.errorName };
87+
}
88+
}
89+
4390
export function DecodeForm() {
4491
const { data: worldData, isLoading: isWorldAbiLoading } = useWorldAbiQuery();
4592
const { data: systemData, isLoading: isSystemAbisLoading } = useSystemAbisQuery();
46-
const [abiItem, setAbiItem] = useState<AbiFunction | AbiError>();
93+
const [decoded, setDecoded] = useState<AbiFunction | AbiError | ResourceItem | SelectorDatabase>();
94+
const [encoded, setEncoded] = useState<string | undefined>();
4795
const form = useForm<z.infer<typeof formSchema>>({
4896
resolver: zodResolver(formSchema),
4997
});
5098

51-
function onSubmit({ selector }: z.infer<typeof formSchema>) {
99+
const onSubmit = async ({ encodedData }: z.infer<typeof formSchema>) => {
100+
setEncoded(encodedData);
101+
if (!encodedData) return;
102+
103+
const selector = encodedData.substring(0, 10);
52104
const worldAbi = worldData?.abi || [];
53105
const systemsAbis = systemData ? Object.values(systemData) : [];
54106
const abis = [worldAbi, ...systemsAbis].flat();
55107

108+
// Try to find matching ABI item
56109
const abiItem = abis.find((item): item is AbiFunction | AbiError => {
57110
if (isAbiFunction(item)) {
58111
return toFunctionSelector(item) === selector;
59-
} else if (isAbiError(item)) {
112+
}
113+
if (isAbiError(item)) {
60114
return getErrorSelector(item) === selector;
61115
}
62116
return false;
63117
});
64118

65-
setAbiItem(abiItem);
66-
}
119+
if (abiItem) {
120+
setDecoded(abiItem);
121+
return;
122+
}
123+
124+
// Try to decode as resource
125+
try {
126+
const resource = hexToResource(encodedData as Hex);
127+
if (resource) {
128+
setDecoded({ type: "resource", id: encodedData, resource });
129+
return;
130+
}
131+
} catch {
132+
// Error decoding resource
133+
}
134+
135+
// Try to find in 4-byte database
136+
if (selector.length !== 10) return;
137+
try {
138+
const response = await fetch(`https://www.4byte.directory/api/v1/signatures/?hex_signature=${selector}`);
139+
const data = await response.json();
140+
if (data.results.length > 0) {
141+
setDecoded({
142+
type: "signature",
143+
selectors: data.results.map((result: { text_signature: string }) => result.text_signature),
144+
});
145+
return;
146+
}
147+
} catch {
148+
// Error fetching 4byte data
149+
}
150+
151+
setDecoded(undefined);
152+
};
153+
154+
const results = useMemo<Result[]>(() => {
155+
if (!decoded || !encoded) return [];
156+
try {
157+
if (decoded.type === "resource") {
158+
return [{ type: decoded.resource.type, label: resourceToLabel(decoded.resource) }];
159+
}
160+
if (decoded.type === "function" || decoded.type === "error") {
161+
try {
162+
const decodedCall = decodeAbiItem(decoded, encoded as Hex);
163+
return [{ type: decoded.type, label: formatAbiItem(decoded), decodedCall }];
164+
} catch {
165+
return [{ type: decoded.type, label: formatAbiItem(decoded) }];
166+
}
167+
}
168+
if (decoded.type === "signature") {
169+
return decoded.selectors.map((selector) => {
170+
const abiItem = parseAbiItem(`function ${selector}`) as AbiFunction;
171+
try {
172+
const decodedCall = decodeAbiItem(abiItem, encoded as Hex);
173+
return { type: "signature", label: selector, decodedCall };
174+
} catch {
175+
return { type: "signature", label: selector };
176+
}
177+
});
178+
}
179+
} catch {
180+
// Error decoding function data
181+
}
182+
return [];
183+
}, [decoded, encoded]);
67184

68185
if (isWorldAbiLoading || isSystemAbisLoading) {
69186
return <Skeleton className="h-[152px] w-full" />;
@@ -74,35 +191,47 @@ export function DecodeForm() {
74191
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
75192
<FormField
76193
control={form.control}
77-
name="selector"
194+
name="encodedData"
78195
render={({ field }) => (
79196
<FormItem>
80-
<FormLabel>Encoded selector</FormLabel>
197+
<FormLabel>Encoded data</FormLabel>
81198
<FormControl>
82199
<Input placeholder="0xf0f0f0f0" type="text" {...field} />
83200
</FormControl>
84-
<FormDescription>Find the function or error by its selector</FormDescription>
201+
<FormDescription>Decode function, error or resource data</FormDescription>
85202
<FormMessage />
86203
</FormItem>
87204
)}
88205
/>
89206

90207
{form.formState.isSubmitted && (
91-
<pre
92-
className={cn("text-md relative mt-4 rounded border border-white/20 p-3 text-sm", {
93-
"border-red-400 bg-red-100": !abiItem,
94-
})}
95-
>
96-
{abiItem ? (
97-
<>
98-
<span className="mr-2 text-sm opacity-50">{abiItem.type === "function" ? "function" : "error"}</span>
99-
<span>{formatAbiItem(abiItem)}</span>
100-
<CopyButton value={JSON.stringify(abiItem, null, 2)} className="absolute right-1.5 top-1.5" />
101-
</>
208+
<>
209+
{results.length > 0 ? (
210+
results.map(({ type, label, decodedCall }, index) => (
211+
<pre key={`call-${index}`} className="text-md relative mt-4 rounded border border-white/20 p-3 text-sm">
212+
<span className="mr-2 text-sm opacity-50">{type}</span>
213+
<span>{label}</span>
214+
<pre className="block whitespace-pre-wrap">
215+
{decodedCall?.abi?.inputs?.map((input, inputIndex) => (
216+
<p key={`input-${inputIndex}`} className={"ml-4"}>
217+
<span className="opacity-50">{input.name ?? `arg ${inputIndex}`}: </span>
218+
<span>{stringifyValue(decodedCall.args[inputIndex])}</span>
219+
</p>
220+
))}
221+
</pre>
222+
<CopyButton value={JSON.stringify(decoded, null, 2)} className="absolute right-1.5 top-1.5" />
223+
</pre>
224+
))
102225
) : (
103-
<span className="text-red-700">No matching function or error found for this selector</span>
226+
<pre
227+
className={cn("text-md relative mt-4 rounded border border-white/20 p-3 text-sm", {
228+
"border-red-400 bg-red-100": !decoded,
229+
})}
230+
>
231+
<span className="text-red-700">No matching function, error or resource found for this data</span>
232+
</pre>
104233
)}
105-
</pre>
234+
</>
106235
)}
107236

108237
<Button type="submit" size="sm">

0 commit comments

Comments
 (0)