From 3a32b11a80cb28f22575219ab8f80e703793ec10 Mon Sep 17 00:00:00 2001 From: kien-ngo Date: Thu, 5 Sep 2024 23:48:30 +0000 Subject: [PATCH] [Dashboard] Tx simulator for Explorer (#4293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## PR-Codex overview This PR adds a new feature to simulate contract transactions before execution, enhancing user experience and safety. ### Detailed summary - Added `useSimulateTransaction` hook for simulating transactions - Implemented simulation functionality in `InteractiveAbiFunction` - Added simulation button and logic for contract functions > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../interactive-abi-function.tsx | 167 +++++++++++++----- 1 file changed, 127 insertions(+), 40 deletions(-) diff --git a/apps/dashboard/src/components/contract-functions/interactive-abi-function.tsx b/apps/dashboard/src/components/contract-functions/interactive-abi-function.tsx index 2a8d4b5a82c..7d7b1eeb44d 100644 --- a/apps/dashboard/src/components/contract-functions/interactive-abi-function.tsx +++ b/apps/dashboard/src/components/contract-functions/interactive-abi-function.tsx @@ -1,3 +1,4 @@ +import { ToolTipLabel } from "@/components/ui/tooltip"; import { ButtonGroup, Code, @@ -15,6 +16,7 @@ import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers"; import { replaceIpfsUrl } from "lib/sdk"; import { useEffect, useId, useMemo } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; +import { FaCircleInfo } from "react-icons/fa6"; import { FiPlay } from "react-icons/fi"; import { toast } from "sonner"; import { @@ -22,9 +24,11 @@ import { prepareContractCall, readContract, resolveMethod, + simulateTransaction, + toSerializableTransaction, toWei, } from "thirdweb"; -import { useSendAndConfirmTransaction } from "thirdweb/react"; +import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react"; import { parseAbiParams, stringify } from "thirdweb/utils"; import { Button, @@ -123,6 +127,60 @@ function useAsyncRead(contract: ThirdwebContract, functionName: string) { ); } +function useSimulateTransaction() { + const from = useActiveAccount()?.address; + return useMutation( + async ({ + contract, + functionName, + params, + value, + }: { + contract: ThirdwebContract; + functionName: string; + params: unknown[]; + value?: bigint; + }) => { + if (!from) { + return toast.error("No account connected"); + } + const transaction = prepareContractCall({ + contract, + method: resolveMethod(functionName), + params, + value, + }); + try { + const [simulateResult, populatedTransaction] = await Promise.all([ + simulateTransaction({ + from, + transaction, + }), + toSerializableTransaction({ + from, + transaction, + }), + ]); + return `--- ✅ Simulation succeeded --- +Result: ${simulateResult.length ? simulateResult.join(", ") : "Method did not return a result."} +Transaction data: +${Object.keys(populatedTransaction) + .map((key) => { + let _val = populatedTransaction[key as keyof typeof populatedTransaction]; + if (key === "value" && !_val) { + _val = 0; + } + return `${key}: ${_val}\n`; + }) + .join("")}`; + } catch (err) { + return `--- ❌ Simulation failed --- +${(err as Error).message || ""}`; + } + }, + ); +} + export const InteractiveAbiFunction: React.FC = ({ abiFunction, contract, @@ -166,6 +224,8 @@ export const InteractiveAbiFunction: React.FC = ({ error: readError, } = useAsyncRead(contract, abiFunction.name); + const txSimulation = useSimulateTransaction(); + const formattedReadData: string = useMemo( () => (readData ? formatResponseData(readData) : ""), [readData], @@ -188,6 +248,43 @@ export const InteractiveAbiFunction: React.FC = ({ } }, [abiFunction, form, readFn]); + const handleContractRead = form.handleSubmit((d) => { + const types = abiFunction.inputs.map((o) => o.type); + const formatted = formatContractCall(d.params); + readFn({ args: formatted, types }); + }); + + const handleContractWrite = form.handleSubmit((d) => { + if (!abiFunction.name) { + return toast.error("Cannot detect function name"); + } + const types = abiFunction.inputs.map((o) => o.type); + const formatted = formatContractCall(d.params); + const params = parseAbiParams(types, formatted); + const transaction = prepareContractCall({ + contract, + method: resolveMethod(abiFunction.name), + params, + value: d.value ? toWei(d.value) : undefined, + }); + mutate(transaction); + }); + + const handleContractSimulation = form.handleSubmit((d) => { + if (!abiFunction.name) { + return toast.error("Cannot detect function name"); + } + const types = abiFunction.inputs.map((o) => o.type); + const formatted = formatContractCall(d.params); + const params = parseAbiParams(types, formatted); + txSimulation.mutate({ + contract, + params, + functionName: abiFunction.name, + value: d.value ? toWei(d.value) : undefined, + }); + }); + return ( = ({ gap={2} as="form" id={formId} - onSubmit={form.handleSubmit((d) => { - if (d.params) { - const formatted = formatContractCall(d.params); - if ( - contract && - (abiFunction.stateMutability === "view" || - abiFunction.stateMutability === "pure") - ) { - const types = abiFunction.inputs.map((o) => o.type); - readFn({ args: formatted, types }); - } else { - if (!abiFunction.name) { - return toast.error("Cannot detect function name"); - } - const types = abiFunction.inputs.map((o) => o.type); - const params = parseAbiParams(types, formatted); - const transaction = prepareContractCall({ - contract, - method: resolveMethod(abiFunction.name), - params, - value: d.value ? toWei(d.value) : undefined, - }); - mutate(transaction); - } - } - })} > {fields.length > 0 && ( <> @@ -311,7 +382,9 @@ export const InteractiveAbiFunction: React.FC = ({ {formatError(error as any)} - ) : data !== undefined || readData !== undefined ? ( + ) : data !== undefined || + readData !== undefined || + txSimulation.data ? ( <> Output @@ -319,7 +392,7 @@ export const InteractiveAbiFunction: React.FC = ({ w="full" position="relative" language="json" - code={formatResponseData(data || readData)} + code={formatResponseData(data || readData || txSimulation.data)} /> {formattedReadData.startsWith("ipfs://") && ( @@ -345,22 +418,36 @@ export const InteractiveAbiFunction: React.FC = ({ rightIcon={} colorScheme="primary" isLoading={readLoading} - type="submit" + onClick={handleContractRead} form={formId} > Run ) : ( - - Execute - + <> + + + Execute + + )}