Skip to content

Commit 1a2b3c8

Browse files
authored
feat(explorer): show event logs for interact function (#3418)
1 parent 6aff69a commit 1a2b3c8

5 files changed

Lines changed: 193 additions & 175 deletions

File tree

.changeset/fresh-chairs-obey.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 functions in the Interact tab now display the emitted logs with the block explorer URL for the submitted transaction.
Lines changed: 153 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
"use client";
22

3-
import { Coins, Eye, Send } from "lucide-react";
4-
import { Abi, AbiFunction } from "viem";
5-
import { useAccount } from "wagmi";
3+
import { Coins, ExternalLinkIcon, Eye, LoaderIcon, Send } from "lucide-react";
4+
import Link from "next/link";
5+
import { useParams } from "next/navigation";
6+
import { toast } from "sonner";
7+
import { Abi, AbiFunction, Address, Hex, decodeEventLog } from "viem";
8+
import { useAccount, useConfig } from "wagmi";
9+
import { readContract, waitForTransactionReceipt, writeContract } from "wagmi/actions";
610
import { z } from "zod";
711
import { useState } from "react";
812
import { useForm } from "react-hook-form";
@@ -12,7 +16,8 @@ import { Button } from "../../../../../../components/ui/Button";
1216
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../../../../components/ui/Form";
1317
import { Input } from "../../../../../../components/ui/Input";
1418
import { Separator } from "../../../../../../components/ui/Separator";
15-
import { useContractMutation } from "./useContractMutation";
19+
import { useChain } from "../../../../hooks/useChain";
20+
import { blockExplorerTransactionUrl } from "../../../../utils/blockExplorerTransactionUrl";
1621

1722
export enum FunctionType {
1823
READ,
@@ -24,6 +29,11 @@ type Props = {
2429
functionAbi: AbiFunction;
2530
};
2631

32+
type DecodedEvent = {
33+
eventName: string | undefined;
34+
args: readonly unknown[] | undefined;
35+
};
36+
2737
const formSchema = z.object({
2838
inputs: z.array(z.string()),
2939
value: z.string().optional(),
@@ -34,10 +44,16 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
3444
functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure"
3545
? FunctionType.READ
3646
: FunctionType.WRITE;
37-
const [result, setResult] = useState<string | null>(null);
3847
const { openConnectModal } = useConnectModal();
39-
const mutation = useContractMutation({ worldAbi, functionAbi, operationType });
48+
const wagmiConfig = useConfig();
4049
const account = useAccount();
50+
const { worldAddress } = useParams();
51+
const { id: chainId } = useChain();
52+
const [isLoading, setIsLoading] = useState(false);
53+
const [result, setResult] = useState<string>();
54+
const [events, setEvents] = useState<DecodedEvent[]>();
55+
const [txHash, setTxHash] = useState<Hex>();
56+
const txUrl = blockExplorerTransactionUrl({ hash: txHash, chainId });
4157

4258
const form = useForm<z.infer<typeof formSchema>>({
4359
resolver: zodResolver(formSchema),
@@ -51,74 +67,142 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
5167
return openConnectModal?.();
5268
}
5369

54-
const mutationResult = await mutation.mutateAsync({
55-
inputs: values.inputs,
56-
value: values.value,
57-
});
70+
setIsLoading(true);
71+
let toastId;
72+
try {
73+
if (operationType === FunctionType.READ) {
74+
const result = await readContract(wagmiConfig, {
75+
abi: worldAbi,
76+
address: worldAddress as Address,
77+
functionName: functionAbi.name,
78+
args: values.inputs,
79+
chainId,
80+
});
81+
82+
setResult(JSON.stringify(result, null, 2));
83+
} else {
84+
toastId = toast.loading("Transaction submitted");
85+
const txHash = await writeContract(wagmiConfig, {
86+
abi: worldAbi,
87+
address: worldAddress as Address,
88+
functionName: functionAbi.name,
89+
args: values.inputs,
90+
...(values.value && { value: BigInt(values.value) }),
91+
chainId,
92+
});
93+
setTxHash(txHash);
5894

59-
if (operationType === FunctionType.READ && "result" in mutationResult) {
60-
setResult(JSON.stringify(mutationResult.result, null, 2));
95+
const receipt = await waitForTransactionReceipt(wagmiConfig, { hash: txHash });
96+
const events = receipt?.logs.map((log) => decodeEventLog({ ...log, abi: worldAbi }));
97+
setEvents(events);
98+
99+
toast.success(`Transaction successful with hash: ${txHash}`, {
100+
id: toastId,
101+
});
102+
}
103+
} catch (error) {
104+
console.error(error);
105+
toast.error((error as Error).message || "Something went wrong. Please try again.", {
106+
id: toastId,
107+
});
108+
} finally {
109+
setIsLoading(false);
61110
}
62111
}
63112

64113
const inputsLabel = functionAbi?.inputs.map((input) => input.type).join(", ");
65114
return (
66-
<Form {...form}>
67-
<form onSubmit={form.handleSubmit(onSubmit)} id={functionAbi.name} className="space-y-4 pb-4">
68-
<h3 className="pt-4 font-semibold">
69-
<span className="text-orange-500">{functionAbi?.name}</span>
70-
<span className="opacity-50">{inputsLabel && ` (${inputsLabel})`}</span>
71-
<span className="ml-2 opacity-50">
72-
{functionAbi.stateMutability === "payable" && <Coins className="mr-2 inline-block h-4 w-4" />}
73-
{(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && (
74-
<Eye className="mr-2 inline-block h-4 w-4" />
75-
)}
76-
{functionAbi.stateMutability === "nonpayable" && <Send className="mr-2 inline-block h-4 w-4" />}
77-
</span>
78-
</h3>
79-
80-
{functionAbi?.inputs.map((input, index) => (
81-
<FormField
82-
key={index}
83-
control={form.control}
84-
name={`inputs.${index}`}
85-
render={({ field }) => (
86-
<FormItem>
87-
<FormLabel>{input.name}</FormLabel>
88-
<FormControl>
89-
<Input placeholder={input.type} {...field} />
90-
</FormControl>
91-
<FormMessage />
92-
</FormItem>
93-
)}
94-
/>
95-
))}
96-
97-
{functionAbi.stateMutability === "payable" && (
98-
<FormField
99-
control={form.control}
100-
name="value"
101-
render={({ field }) => (
102-
<FormItem>
103-
<FormLabel>ETH value</FormLabel>
104-
<FormControl>
105-
<Input placeholder="uint256" {...field} />
106-
</FormControl>
107-
<FormMessage />
108-
</FormItem>
109-
)}
110-
/>
111-
)}
112-
113-
<Button type="submit" disabled={mutation.isPending}>
114-
{(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && "Read"}
115-
{(functionAbi.stateMutability === "payable" || functionAbi.stateMutability === "nonpayable") && "Write"}
116-
</Button>
117-
118-
{result && <pre className="text-md rounded border p-3 text-sm">{result}</pre>}
119-
</form>
120-
121-
<Separator />
122-
</Form>
115+
<div className="pb-6">
116+
<Form {...form}>
117+
<form onSubmit={form.handleSubmit(onSubmit)} id={functionAbi.name} className="space-y-4">
118+
<h3 className="font-semibold">
119+
<span className="text-orange-500">{functionAbi?.name}</span>
120+
<span className="opacity-50">{inputsLabel && ` (${inputsLabel})`}</span>
121+
<span className="ml-2 opacity-50">
122+
{functionAbi.stateMutability === "payable" && <Coins className="mr-2 inline-block h-4 w-4" />}
123+
{(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && (
124+
<Eye className="mr-2 inline-block h-4 w-4" />
125+
)}
126+
{functionAbi.stateMutability === "nonpayable" && <Send className="mr-2 inline-block h-4 w-4" />}
127+
</span>
128+
</h3>
129+
130+
{functionAbi?.inputs.map((input, index) => (
131+
<FormField
132+
key={index}
133+
control={form.control}
134+
name={`inputs.${index}`}
135+
render={({ field }) => (
136+
<FormItem>
137+
<FormLabel>{input.name}</FormLabel>
138+
<FormControl>
139+
<Input placeholder={input.type} {...field} />
140+
</FormControl>
141+
<FormMessage />
142+
</FormItem>
143+
)}
144+
/>
145+
))}
146+
147+
{functionAbi.stateMutability === "payable" && (
148+
<FormField
149+
control={form.control}
150+
name="value"
151+
render={({ field }) => (
152+
<FormItem>
153+
<FormLabel>ETH value</FormLabel>
154+
<FormControl>
155+
<Input placeholder="uint256" {...field} />
156+
</FormControl>
157+
<FormMessage />
158+
</FormItem>
159+
)}
160+
/>
161+
)}
162+
163+
<Button type="submit" size="sm" disabled={isLoading || !account.isConnected}>
164+
{isLoading && <LoaderIcon className="-ml-1 mr-2 h-4 w-4 animate-spin" />}
165+
{operationType === FunctionType.READ ? "Read" : "Write"}
166+
</Button>
167+
</form>
168+
</Form>
169+
170+
{result && <pre className="text-md mt-4 rounded border p-3 text-sm">{result}</pre>}
171+
{events && (
172+
<div className="mt-4 flex-grow break-all border border-white/20 p-2 pb-3">
173+
<ul>
174+
{events.map((event, idx) => (
175+
<li key={idx}>
176+
{event.eventName && <span className="text-xs">{event.eventName}:</span>}
177+
{event.args && (
178+
<ul className="list-inside">
179+
{Object.entries(event.args).map(([key, value]) => (
180+
<li key={key} className="mt-1 flex">
181+
<span className="text-xs text-white/60">{key}:</span>{" "}
182+
<span className="text-xs">{String(value)}</span>
183+
</li>
184+
))}
185+
</ul>
186+
)}
187+
{idx < events.length - 1 && <Separator className="my-4" />}
188+
</li>
189+
))}
190+
</ul>
191+
</div>
192+
)}
193+
{txUrl && (
194+
<div className="mt-3">
195+
<Link
196+
href={txUrl}
197+
target="_blank"
198+
className="flex items-center text-xs text-muted-foreground hover:underline"
199+
>
200+
<ExternalLinkIcon className="mr-2 h-3 w-3" /> View on block explorer
201+
</Link>
202+
</div>
203+
)}
204+
205+
<Separator className="mt-6" />
206+
</div>
123207
);
124208
}

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function InteractForm() {
4444
{!isFetched &&
4545
Array.from({ length: 10 }).map((_, index) => {
4646
return (
47-
<li key={index} className="pt-2">
47+
<li key={index} className="pr-4 pt-2">
4848
<Skeleton className="h-[25px]" />
4949
</li>
5050
);

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts

Lines changed: 0 additions & 80 deletions
This file was deleted.

0 commit comments

Comments
 (0)