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" ;
412import { formatAbiItem } from "viem/utils" ;
513import * as z from "zod" ;
6- import { useState } from "react" ;
14+ import { useMemo , useState } from "react" ;
715import "react18-json-view/src/dark.css" ;
816import "react18-json-view/src/style.css" ;
917import { useForm } from "react-hook-form" ;
1018import { zodResolver } from "@hookform/resolvers/zod" ;
19+ import { Resource , hexToResource , resourceToLabel } from "@latticexyz/common" ;
1120import { CopyButton } from "../../../../../../components/CopyButton" ;
1221import { Button } from "../../../../../../components/ui/Button" ;
1322import {
@@ -27,9 +36,23 @@ import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";
2736import { getErrorSelector } from "./getErrorSelector" ;
2837
2938type 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
3154const formSchema = z . object ( {
32- selector : z . string ( ) . min ( 1 ) . optional ( ) ,
55+ encodedData : z . string ( ) . min ( 1 ) . optional ( ) ,
3356} ) ;
3457
3558function 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+
4390export 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