Skip to content

Commit

Permalink
[Dashboard] Tx simulator for Explorer (#4293)
Browse files Browse the repository at this point in the history
<!-- start pr-codex -->

## 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}`

<!-- end pr-codex -->
  • Loading branch information
kien-ngo committed Sep 5, 2024
1 parent dbebfb4 commit 3a32b11
Showing 1 changed file with 127 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ToolTipLabel } from "@/components/ui/tooltip";
import {
ButtonGroup,
Code,
Expand All @@ -15,16 +16,19 @@ 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 {
type ThirdwebContract,
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,
Expand Down Expand Up @@ -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<InteractiveAbiFunctionProps> = ({
abiFunction,
contract,
Expand Down Expand Up @@ -166,6 +224,8 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
error: readError,
} = useAsyncRead(contract, abiFunction.name);

const txSimulation = useSimulateTransaction();

const formattedReadData: string = useMemo(
() => (readData ? formatResponseData(readData) : ""),
[readData],
Expand All @@ -188,6 +248,43 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
}
}, [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 (
<FormProvider {...form}>
<Card
Expand All @@ -210,32 +307,6 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
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 && (
<>
Expand Down Expand Up @@ -311,15 +382,17 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
{formatError(error as any)}
</Text>
</>
) : data !== undefined || readData !== undefined ? (
) : data !== undefined ||
readData !== undefined ||
txSimulation.data ? (
<>
<Divider />
<Heading size="label.sm">Output</Heading>
<CodeBlock
w="full"
position="relative"
language="json"
code={formatResponseData(data || readData)}
code={formatResponseData(data || readData || txSimulation.data)}
/>
{formattedReadData.startsWith("ipfs://") && (
<Text size="label.sm">
Expand All @@ -345,22 +418,36 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
rightIcon={<Icon as={FiPlay} />}
colorScheme="primary"
isLoading={readLoading}
type="submit"
onClick={handleContractRead}
form={formId}
>
Run
</Button>
) : (
<TransactionButton
isDisabled={!abiFunction}
colorScheme="primary"
transactionCount={1}
isLoading={mutationLoading}
type="submit"
form={formId}
>
Execute
</TransactionButton>
<>
<Button
onClick={handleContractSimulation}
isDisabled={!abiFunction}
isLoading={txSimulation.isLoading}
>
<ToolTipLabel label="Simulate the transaction to see its potential outcome without actually sending it to the network. This action doesn't cost gas.">
<span className="mr-3">
<FaCircleInfo size={20} />
</span>
</ToolTipLabel>
Simulate
</Button>
<TransactionButton
isDisabled={!abiFunction}
colorScheme="primary"
transactionCount={1}
isLoading={mutationLoading}
form={formId}
onClick={handleContractWrite}
>
Execute
</TransactionButton>
</>
)}
</ButtonGroup>
</Card>
Expand Down

0 comments on commit 3a32b11

Please sign in to comment.