diff --git a/.changeset/good-eyes-nail.md b/.changeset/good-eyes-nail.md new file mode 100644 index 0000000000..fd33f62862 --- /dev/null +++ b/.changeset/good-eyes-nail.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/explorer": patch +--- + +Interact tab now displays decoded ABI errors for failed transactions. diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx index 8d54c757fa..5c99e7522a 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx @@ -1,7 +1,7 @@ "use client"; import { Coins, Eye, Send } from "lucide-react"; -import { AbiFunction } from "viem"; +import { Abi, AbiFunction } from "viem"; import { useAccount } from "wagmi"; import { z } from "zod"; import { useState } from "react"; @@ -20,7 +20,8 @@ export enum FunctionType { } type Props = { - abi: AbiFunction; + worldAbi: Abi; + functionAbi: AbiFunction; }; const formSchema = z.object({ @@ -28,12 +29,14 @@ const formSchema = z.object({ value: z.string().optional(), }); -export function FunctionField({ abi }: Props) { +export function FunctionField({ worldAbi, functionAbi }: Props) { const operationType: FunctionType = - abi.stateMutability === "view" || abi.stateMutability === "pure" ? FunctionType.READ : FunctionType.WRITE; + functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure" + ? FunctionType.READ + : FunctionType.WRITE; const [result, setResult] = useState(null); const { openConnectModal } = useConnectModal(); - const mutation = useContractMutation({ abi, operationType }); + const mutation = useContractMutation({ worldAbi, functionAbi, operationType }); const account = useAccount(); const form = useForm>({ @@ -58,23 +61,23 @@ export function FunctionField({ abi }: Props) { } } - const inputsLabel = abi?.inputs.map((input) => input.type).join(", "); + const inputsLabel = functionAbi?.inputs.map((input) => input.type).join(", "); return (
- +

- {abi?.name} + {functionAbi?.name} {inputsLabel && ` (${inputsLabel})`} - {abi.stateMutability === "payable" && } - {(abi.stateMutability === "view" || abi.stateMutability === "pure") && ( + {functionAbi.stateMutability === "payable" && } + {(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && ( )} - {abi.stateMutability === "nonpayable" && } + {functionAbi.stateMutability === "nonpayable" && }

- {abi?.inputs.map((input, index) => ( + {functionAbi?.inputs.map((input, index) => ( ))} - {abi.stateMutability === "payable" && ( + {functionAbi.stateMutability === "payable" && ( - {(abi.stateMutability === "view" || abi.stateMutability === "pure") && "Read"} - {(abi.stateMutability === "payable" || abi.stateMutability === "nonpayable") && "Write"} + {(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && "Read"} + {(functionAbi.stateMutability === "payable" || functionAbi.stateMutability === "nonpayable") && "Write"} {result &&
{result}
} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx similarity index 86% rename from packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx index dd9ba09c07..763a6e04d3 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx @@ -3,7 +3,7 @@ import { Coins, Eye, Send } from "lucide-react"; import { useQueryState } from "nuqs"; import { AbiFunction } from "viem"; -import { useDeferredValue } from "react"; +import { useDeferredValue, useMemo } from "react"; import { Input } from "../../../../../../components/ui/Input"; import { Separator } from "../../../../../../components/ui/Separator"; import { Skeleton } from "../../../../../../components/ui/Skeleton"; @@ -12,14 +12,17 @@ import { useHashState } from "../../../../hooks/useHashState"; import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery"; import { FunctionField } from "./FunctionField"; -export function Form() { +export function InteractForm() { const [hash] = useHashState(); const { data, isFetched } = useWorldAbiQuery(); const [filterValue, setFilterValue] = useQueryState("function", { defaultValue: "" }); const deferredFilterValue = useDeferredValue(filterValue); - const filteredFunctions = data?.abi?.filter( - (item) => item.type === "function" && item.name.toLowerCase().includes(deferredFilterValue.toLowerCase()), - ); + const filteredFunctions = useMemo(() => { + if (!data?.abi) return []; + return data.abi.filter( + (item) => item.type === "function" && item.name.toLowerCase().includes(deferredFilterValue.toLowerCase()), + ); + }, [data?.abi, deferredFilterValue]); return ( <> @@ -87,9 +90,10 @@ export function Form() { )} - {filteredFunctions?.map((abi) => { - return ; - })} + {data?.abi && + filteredFunctions.map((abi) => ( + + ))} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/page.tsx index 01dc779332..9e2234950a 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/page.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/page.tsx @@ -1,10 +1,10 @@ import { Metadata } from "next"; -import { Form } from "./Form"; +import { InteractForm } from "./InteractForm"; export const metadata: Metadata = { title: "Interact", }; export default async function InteractPage() { - return ; + return ; } diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts index d38ddb16c8..3289e8cd6b 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts @@ -8,11 +8,12 @@ import { useChain } from "../../../../hooks/useChain"; import { FunctionType } from "./FunctionField"; type UseContractMutationProps = { - abi: AbiFunction; + worldAbi: Abi; + functionAbi: AbiFunction; operationType: FunctionType; }; -export function useContractMutation({ abi, operationType }: UseContractMutationProps) { +export function useContractMutation({ worldAbi, functionAbi, operationType }: UseContractMutationProps) { const { worldAddress } = useParams(); const { id: chainId } = useChain(); const queryClient = useQueryClient(); @@ -23,9 +24,9 @@ export function useContractMutation({ abi, operationType }: UseContractMutationP mutationFn: async ({ inputs, value }: { inputs: unknown[]; value?: string }) => { if (operationType === FunctionType.READ) { const result = await readContract(wagmiConfig, { - abi: [abi] as Abi, + abi: worldAbi, address: worldAddress as Hex, - functionName: abi.name, + functionName: functionAbi.name, args: inputs, chainId, }); @@ -33,9 +34,9 @@ export function useContractMutation({ abi, operationType }: UseContractMutationP return { result }; } else { const txHash = await writeContract(wagmiConfig, { - abi: [abi] as Abi, + abi: worldAbi, address: worldAddress as Hex, - functionName: abi.name, + functionName: functionAbi.name, args: inputs, ...(value && { value: BigInt(value) }), chainId,