diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index aa1222e..e1274d4 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,8 +1,22 @@ import { withThemeByClassName } from "@storybook/addon-themes"; import type { Preview } from "@storybook/react-vite"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React from "react"; import "../tailwind.css"; import { ClipboardProvider } from "../components/providers/clipboard-provider"; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + mutations: { + retry: false, + }, + }, +}); + const preview: Preview = { parameters: { controls: { @@ -22,11 +36,13 @@ const preview: Preview = { defaultTheme: "light", }), (Story) => ( -
- - - -
+ +
+ + + +
+
), ], }; diff --git a/components/contract-execution/contract-execution-tabs/function-item.tsx b/components/contract-execution/contract-execution-tabs/function-item.tsx index 75be755..8f3dba8 100644 --- a/components/contract-execution/contract-execution-tabs/function-item.tsx +++ b/components/contract-execution/contract-execution-tabs/function-item.tsx @@ -1,22 +1,12 @@ -import { memo, useCallback, useState } from "react"; -import { FormProvider } from "react-hook-form"; +import { memo } from "react"; import type { AbiFunction } from "viem"; -import { AbiItemFormWithPreview } from "../../abi-form/abi-item-form-with-preview.js"; import { AccordionContent, AccordionItem, AccordionTrigger, } from "../../shadcn/accordion.js"; -import { - ActionButtons, - ConnectWalletAlert, - DefaultResultDisplay, - MsgSenderInput, -} from "../shared/components.js"; -import { useMsgSenderForm } from "../shared/form-utils.js"; -import type { BaseExecutionProps, ExecutionParams } from "../shared/types.js"; -import { useFunctionExecution } from "../shared/use-function-execution.js"; -import { isWriteFunction } from "../shared/utils.js"; +import { ExecutionForm } from "../shared/components/execution-form.js"; +import type { BaseExecutionProps, ExecutionParams } from "../types.js"; interface FunctionItemProps extends BaseExecutionProps { func: AbiFunction; @@ -42,42 +32,6 @@ export const FunctionItem = memo( addressRenderer, onHashClick, }: FunctionItemProps) => { - const [callData, setCallData] = useState(""); - const { result, isSimulating, isExecuting, simulate, execute } = - useFunctionExecution(); - const { form, msgSender } = useMsgSenderForm(sender); - - const isWrite = isWriteFunction(func); - - const handleCallDataChange = useCallback( - (newCallData: string | undefined) => { - setCallData(newCallData || ""); - }, - [], - ); - - const handleSimulate = () => { - simulate({ - abiFunction: func, - callData, - msgSender, - onQuery, - onWrite, - onSimulate, - }); - }; - - const handleExecute = () => { - execute({ - abiFunction: func, - callData, - msgSender, - onQuery, - onWrite, - onSimulate, - }); - }; - const functionKey = `${func.name}-${index}`; return ( @@ -91,55 +45,23 @@ export const FunctionItem = memo( - -
- {isWrite && } - - { - const callData = data.data?.toString() ?? undefined; - handleCallDataChange(callData); - }} - abiFunction={func} - address={address} - sender={sender || address} - chainId={chainId} - defaultCalldata={callData as `0x${string}` | undefined} - ArgProps={ - addressRenderer - ? { - addressRenderer, - } - : undefined - } - /> - - {isWrite && requiresConnection && !isConnected && ( - - )} - - - - {result && ( - - )} -
-
+
); diff --git a/components/contract-execution/contract-execution-tabs/index.tsx b/components/contract-execution/contract-execution-tabs/index.tsx index da69357..f3a40d2 100644 --- a/components/contract-execution/contract-execution-tabs/index.tsx +++ b/components/contract-execution/contract-execution-tabs/index.tsx @@ -5,7 +5,7 @@ import { cn } from "../../../lib/utils.js"; import { Accordion } from "../../shadcn/accordion.js"; import { Input } from "../../shadcn/input.js"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../shadcn/tabs.js"; -import type { ContractExecutionTabsProps } from "../shared/types.js"; +import type { ContractExecutionTabsProps } from "../types.js"; import { FunctionItem } from "./function-item.js"; import { RawOperations } from "./raw-tab.js"; import { SignatureOperations } from "./signature-tab.js"; @@ -213,6 +213,7 @@ export function ContractExecutionTabs({ isConnected={isConnected} onQuery={onQuery} onWrite={onWrite} + onSimulate={onSimulate} addressRenderer={addressRenderer} onHashClick={onHashClick} /> diff --git a/components/contract-execution/contract-execution-tabs/raw-tab.tsx b/components/contract-execution/contract-execution-tabs/raw-tab.tsx index e8807a4..714bbdb 100644 --- a/components/contract-execution/contract-execution-tabs/raw-tab.tsx +++ b/components/contract-execution/contract-execution-tabs/raw-tab.tsx @@ -1,26 +1,16 @@ -import { useCallback, useState } from "react"; -import { FormProvider } from "react-hook-form"; -import type { Hex } from "viem"; -import { AbiItemFormWithPreview } from "../../abi-form/abi-item-form-with-preview.js"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "../../shadcn/accordion.js"; -import { Button } from "../../shadcn/button.js"; -import { - ConnectWalletAlert, - DefaultResultDisplay, - MsgSenderInput, -} from "../shared/components.js"; -import { useMsgSenderForm } from "../shared/form-utils.js"; -import type { BaseExecutionProps, ExecutionParams } from "../shared/types.js"; -import { useRawExecution } from "../shared/use-raw-execution.js"; +import { ExecutionForm } from "../shared/components/execution-form.js"; +import type { BaseExecutionProps, ExecutionParams } from "../types.js"; interface RawOperationsProps extends BaseExecutionProps { onQuery: (params: ExecutionParams) => Promise<`0x${string}`>; onWrite: (params: ExecutionParams) => Promise<`0x${string}`>; + onSimulate?: (params: ExecutionParams) => Promise<`0x${string}`>; } export function RawOperations({ @@ -32,6 +22,7 @@ export function RawOperations({ isConnected, onQuery, onWrite, + onSimulate, addressRenderer, onHashClick, }: RawOperationsProps) { @@ -46,7 +37,7 @@ export function RawOperations({ addresses={addresses} requiresConnection={requiresConnection} isConnected={isConnected} - onExecute={onQuery} + onQuery={onQuery} addressRenderer={addressRenderer} onHashClick={onHashClick} /> @@ -58,7 +49,8 @@ export function RawOperations({ addresses={addresses} requiresConnection={requiresConnection} isConnected={isConnected} - onExecute={onWrite} + onWrite={onWrite} + onSimulate={onSimulate} addressRenderer={addressRenderer} onHashClick={onHashClick} /> @@ -69,7 +61,9 @@ export function RawOperations({ interface RawOperationItemProps extends BaseExecutionProps { type: "call" | "transaction"; - onExecute: (params: ExecutionParams) => Promise<`0x${string}`>; + onQuery?: (params: ExecutionParams) => Promise<`0x${string}`>; + onWrite?: (params: ExecutionParams) => Promise<`0x${string}`>; + onSimulate?: (params: ExecutionParams) => Promise<`0x${string}`>; } function RawOperationItem({ @@ -80,41 +74,18 @@ function RawOperationItem({ addresses, requiresConnection, isConnected, - onExecute, + onQuery, + onWrite, + onSimulate, addressRenderer, onHashClick, }: RawOperationItemProps) { - const [callData, setCallData] = useState(""); - const [value, setValue] = useState(); - const { form, msgSender } = useMsgSenderForm(sender); - - const isWrite = type === "transaction"; - const { - result, - isExecuting, - execute: executeRaw, - } = useRawExecution({ - isWrite, - onExecute, - }); const title = type === "call" ? "Raw Call" : "Raw Transaction"; const description = type === "call" ? "Execute eth_call with arbitrary calldata" : "Send transaction with arbitrary calldata"; - const handleCallDataChange = useCallback( - ({ data, value: newValue }: { data?: Hex; value?: bigint }) => { - setCallData(data || ""); - setValue(newValue); - }, - [], - ); - - const handleExecute = () => { - executeRaw({ callData, value, msgSender }); - }; - return ( - -
- {isWrite && } - - - - {isWrite && requiresConnection && !isConnected && ( - - )} - -
- -
- - {result && ( - - )} -
-
+
); diff --git a/components/contract-execution/contract-execution-tabs/signature-tab.tsx b/components/contract-execution/contract-execution-tabs/signature-tab.tsx index 58da505..b74392c 100644 --- a/components/contract-execution/contract-execution-tabs/signature-tab.tsx +++ b/components/contract-execution/contract-execution-tabs/signature-tab.tsx @@ -1,39 +1,28 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { useCallback, useMemo, useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import type { AbiFunction, Address, Hex } from "viem"; +import { useMemo } from "react"; +import { useForm } from "react-hook-form"; +import type { AbiFunction, Hex } from "viem"; import { parseAbiItem } from "viem"; import { z } from "zod"; -import { AbiItemFormWithPreview } from "../../abi-form/abi-item-form-with-preview.js"; import { Input } from "../../shadcn/input.js"; import { Label } from "../../shadcn/label.js"; -import { - ActionButtons, - ConnectWalletAlert, - DefaultResultDisplay, - MsgSenderInput, -} from "../shared/components.js"; -import { msgSenderSchema } from "../shared/form-utils.js"; -import type { BaseExecutionProps, ExecutionParams } from "../shared/types.js"; -import { useFunctionExecution } from "../shared/use-function-execution.js"; -import { isWriteFunction } from "../shared/utils.js"; - -const signatureFormSchema = z - .object({ - signature: z.string().refine( - (val) => { - if (!val) return false; - try { - parseAbiItem(val); - return true; - } catch { - return false; - } - }, - { message: "Invalid function signature" }, - ), - }) - .merge(msgSenderSchema); +import { ExecutionForm } from "../shared/components/execution-form.js"; +import type { BaseExecutionProps, ExecutionParams } from "../types.js"; + +const signatureFormSchema = z.object({ + signature: z.string().refine( + (val) => { + if (!val) return false; + try { + parseAbiItem(val); + return true; + } catch { + return false; + } + }, + { message: "Invalid function signature" }, + ), +}); interface SignatureOperationsProps extends BaseExecutionProps { onQuery: (params: ExecutionParams) => Promise; @@ -54,25 +43,15 @@ export function SignatureOperations({ addressRenderer, onHashClick, }: SignatureOperationsProps) { - const [callData, setCallData] = useState(""); - const { result, isSimulating, isExecuting, simulate, execute } = - useFunctionExecution(); - const form = useForm({ mode: "onChange", resolver: zodResolver(signatureFormSchema), defaultValues: { signature: "", - msgSender: "", }, }); const signature = form.watch("signature"); - const msgSenderValue = form.watch("msgSender"); - const msgSender: Address | undefined = msgSenderValue - ? (msgSenderValue as Address) - : undefined; - const isValidSignature = form.getFieldState("signature").invalid === false && signature.length > 0; @@ -85,106 +64,47 @@ export function SignatureOperations({ } }, [isValidSignature, signature]); - const isWrite = isWriteFunction(parsedAbiFunction); - - const handleCallDataChange = useCallback( - ({ data }: { data?: Hex; value?: bigint }) => { - setCallData(data || ""); - }, - [], - ); - - const handleSimulate = () => { - if (!parsedAbiFunction || !callData) return; - simulate({ - abiFunction: parsedAbiFunction, - callData, - msgSender, - onQuery, - onWrite, - onSimulate, - }); - }; - - const handleExecute = () => { - if (!parsedAbiFunction || !callData) return; - execute({ - abiFunction: parsedAbiFunction, - callData, - msgSender, - onQuery, - onWrite, - onSimulate, - }); - }; - return ( -
- -
-
- - - {form.formState.errors.signature && ( -

- {form.formState.errors.signature.message} -

- )} -
- - {isValidSignature && parsedAbiFunction && ( - <> - {isWrite && } - - - - {isWrite && requiresConnection && !isConnected && ( - - )} - - - - {result && ( - - )} - +
+
+
+ + + {form.formState.errors.signature && ( +

+ {form.formState.errors.signature.message} +

)}
- + + {isValidSignature && parsedAbiFunction && ( + + )} +
); } diff --git a/components/contract-execution/resend-transaction/index.tsx b/components/contract-execution/resend-transaction/index.tsx index ed2d63f..b911495 100644 --- a/components/contract-execution/resend-transaction/index.tsx +++ b/components/contract-execution/resend-transaction/index.tsx @@ -1,19 +1,8 @@ import { useMemo } from "react"; -import { FormProvider } from "react-hook-form"; import type { Abi, AbiFunction, Address, Hex } from "viem"; import { decodeFunctionData } from "viem"; -import { AbiItemFormWithPreview } from "../../abi-form/abi-item-form-with-preview.js"; -import { - ActionButtons, - ConnectWalletAlert, - DefaultResultDisplay, - MsgSenderInput, -} from "../shared/components.js"; -import { useMsgSenderForm } from "../shared/form-utils.js"; -import type { ExecutionParams } from "../shared/types.js"; -import { useFunctionExecution } from "../shared/use-function-execution.js"; -import { useRawExecution } from "../shared/use-raw-execution.js"; -import { isWriteFunction } from "../shared/utils.js"; +import { ExecutionForm } from "../shared/components/execution-form.js"; +import type { ExecutionParams } from "../types.js"; export interface ResendTransactionProps { to: Address; @@ -25,7 +14,6 @@ export interface ResendTransactionProps { addresses?: any[]; requiresConnection?: boolean; isConnected?: boolean; - onQuery: (params: ExecutionParams) => Promise; onWrite: (params: ExecutionParams) => Promise; onSimulate?: (params: ExecutionParams) => Promise; addressRenderer?: (address: Address) => React.ReactNode; @@ -42,16 +30,13 @@ export function ResendTransaction({ addresses, requiresConnection = true, isConnected = false, - onQuery, onWrite, onSimulate, addressRenderer, onHashClick, }: ResendTransactionProps) { - const { form, msgSender } = useMsgSenderForm(sender); - const decodedFunction = useMemo(() => { - if (!abi || input === "0x") return null; + if (!abi || input === "0x") return undefined; try { const decoded = decodeFunctionData({ abi, data: input }); @@ -60,98 +45,30 @@ export function ResendTransaction({ item.type === "function" && item.name === decoded.functionName, ) as AbiFunction | undefined; - return abiFunction || null; + return abiFunction; } catch { - return null; + return undefined; } }, [abi, input]); - const isWrite = isWriteFunction(decodedFunction); - - const functionExecution = useFunctionExecution(); - const rawExecution = useRawExecution({ - isWrite: true, - onExecute: onWrite, - }); - - const handleSimulate = () => { - if (decodedFunction && onSimulate) { - functionExecution.simulate({ - abiFunction: decodedFunction, - callData: input, - msgSender, - onQuery, + return ( + { - if (decodedFunction) { - functionExecution.execute({ - abiFunction: decodedFunction, - callData: input, - msgSender, - onQuery, - onWrite, - }); - } else { - rawExecution.execute({ - callData: input, - value, - msgSender, - }); - } - }; - - return ( - -
- - - - - {requiresConnection && !isConnected && } - - - - {decodedFunction && functionExecution.result && ( - - )} - - {!decodedFunction && rawExecution.result && ( - - )} -
-
+ }} + className="space-y-4" + /> ); } diff --git a/components/contract-execution/shared/components.tsx b/components/contract-execution/shared/components/components.tsx similarity index 64% rename from components/contract-execution/shared/components.tsx rename to components/contract-execution/shared/components/components.tsx index cb0adc4..625f5aa 100644 --- a/components/contract-execution/shared/components.tsx +++ b/components/contract-execution/shared/components/components.tsx @@ -1,22 +1,47 @@ import clsx from "clsx"; import { ChevronDown, ChevronUp, ExternalLink, Info } from "lucide-react"; import { useState } from "react"; -import { Form } from "../../form/index.js"; -import { Alert, AlertDescription, AlertTitle } from "../../shadcn/alert.js"; -import { Button } from "../../shadcn/button.js"; -import type { InternalResult } from "./use-function-execution.js"; +import { Form } from "../../../form/index.js"; +import { Alert, AlertDescription, AlertTitle } from "../../../shadcn/alert.js"; +import { Button } from "../../../shadcn/button.js"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../../../shadcn/collapsible.js"; +import type { InternalResult } from "../hooks/use-execution-mutations.js"; + +export function OptionalInputs() { + const [isOpen, setIsOpen] = useState(false); -export function MsgSenderInput() { return ( -
- Msg Sender (Optional) - } - placeholder="0x..." - className="w-full" - /> +
+ + + + Advanced Options + + {isOpen ? ( + + ) : ( + + )} + + +
+ + Msg Sender (Optional) + + } + placeholder="0x..." + className="w-full" + /> +
+
+
); } @@ -35,33 +60,31 @@ export function ConnectWalletAlert() { interface ActionButtonsProps { isWrite: boolean; - callData: string | undefined; - isSimulating: boolean; - isExecuting: boolean; + callData: `0x${string}` | undefined; + isLoading: boolean; isConnected: boolean; - hasSimulate: boolean; simulate?: () => void; - execute: () => void; + query?: () => void; + write?: () => void; } export function ActionButtons({ isWrite, callData, - isSimulating, - isExecuting, + isLoading, isConnected, - hasSimulate, simulate, - execute, + query, + write, }: ActionButtonsProps) { return (
{isWrite ? ( <> - {hasSimulate && ( + {simulate && ( @@ -78,8 +101,8 @@ export function ActionButtons({ ) : ( @@ -127,11 +150,7 @@ export function DefaultResultDisplay({ {result.cleanResult && (
- {result.type === "call" - ? "Result" - : result.type === "simulation" - ? "Simulation Result" - : "Result"} + Result
)} - - {!result.cleanResult && !result.error && result.data && ( -
-          {result.data}
-        
- )}
); } diff --git a/components/contract-execution/shared/components/execution-form.tsx b/components/contract-execution/shared/components/execution-form.tsx new file mode 100644 index 0000000..8dec030 --- /dev/null +++ b/components/contract-execution/shared/components/execution-form.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import { FormProvider } from "react-hook-form"; +import type { AbiFunction, Address } from "viem"; +import { AbiItemFormWithPreview } from "../../../abi-form/abi-item-form-with-preview.js"; +import type { AddressData } from "../../../address-autocomplete-input.js"; +import type { ExecutionParams } from "../../types.js"; +import { useExecutionState } from "../hooks/use-execution-state.js"; +import { useMsgSenderForm } from "../utils/form-utils.js"; +import { + ActionButtons, + ConnectWalletAlert, + DefaultResultDisplay, + OptionalInputs, +} from "./components.js"; + +interface ExecutionFormProps { + abiFunction?: AbiFunction | "raw" | "rawCall" | "signature" | null; + signature?: string; + parsedAbiFunction?: AbiFunction | null; + address: Address; + chainId: number; + sender?: Address; + addresses?: AddressData[]; + requiresConnection: boolean; + isConnected: boolean; + addressRenderer?: (address: Address) => React.ReactNode; + onHashClick?: (hash: string) => void; + defaultCalldata?: `0x${string}`; + defaultEther?: bigint; + executionParams: { + onQuery?: (params: ExecutionParams) => Promise<`0x${string}`>; + onWrite?: (params: ExecutionParams) => Promise<`0x${string}`>; + onSimulate?: (params: ExecutionParams) => Promise<`0x${string}`>; + }; + className?: string; +} + +export function ExecutionForm({ + abiFunction, + signature, + parsedAbiFunction, + address, + chainId, + sender, + addresses, + requiresConnection, + isConnected, + addressRenderer, + onHashClick, + defaultCalldata, + defaultEther, + executionParams, + className, +}: ExecutionFormProps) { + const { form, msgSender } = useMsgSenderForm(sender); + + const executionAbiFunction = + parsedAbiFunction ?? + (abiFunction && typeof abiFunction === "object" ? abiFunction : undefined); + + const execution = useExecutionState({ + abiFunction: executionAbiFunction, + defaultCallData: defaultCalldata, + defaultValue: defaultEther, + msgSender, + ...executionParams, + }); + + const finalIsWrite = + abiFunction === "raw" + ? true + : abiFunction === "rawCall" + ? false + : execution.isWrite; + + return ( + +
+ + + {finalIsWrite && requiresConnection && !isConnected && ( + + )} + + + + + + {execution.result && ( + + )} +
+
+ ); +} diff --git a/components/contract-execution/shared/hooks/use-execution-mutations.ts b/components/contract-execution/shared/hooks/use-execution-mutations.ts new file mode 100644 index 0000000..ef1cb4d --- /dev/null +++ b/components/contract-execution/shared/hooks/use-execution-mutations.ts @@ -0,0 +1,148 @@ +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import type { AbiFunction, Address, Hex } from "viem"; +import { decodeFunctionResult } from "viem"; +import type { ExecutionParams } from "../../types.js"; +import { formatDecodedResult } from "../utils/utils.js"; + +export type InternalResult = { + type: "read" | "simulate" | "write" | "error"; + data?: string; + hash?: Hex; + cleanResult?: string; + error?: string; +}; + +function decodeResult( + abiFunction: AbiFunction | undefined, + rawResult: Hex, +): string | undefined { + if (!abiFunction) return undefined; + try { + const decoded = decodeFunctionResult({ + abi: [abiFunction], + functionName: abiFunction.name, + data: rawResult, + }); + return formatDecodedResult(decoded); + } catch { + return undefined; + } +} + +interface BaseExecutionParams { + abiFunction?: AbiFunction; + callData: Hex; + value?: bigint; + msgSender: Address | undefined; +} + +interface ReadParams extends BaseExecutionParams { + onQuery: (params: ExecutionParams) => Promise; +} + +interface SimulateParams extends BaseExecutionParams { + onSimulate: (params: ExecutionParams) => Promise; +} + +interface WriteParams extends BaseExecutionParams { + onWrite: (params: ExecutionParams) => Promise; +} + +export function useExecutionMutations() { + const [result, setResult] = useState(null); + + const readMutation = useMutation({ + mutationFn: async ({ + abiFunction, + callData, + value, + msgSender, + onQuery, + }: ReadParams) => { + const rawResult = await onQuery({ + abiFunction, + callData, + value, + msgSender, + }); + const cleanResult = decodeResult(abiFunction, rawResult); + return { + type: "read" as const, + data: rawResult, + cleanResult: cleanResult || rawResult, + }; + }, + onSuccess: (data) => setResult(data), + onError: (error) => + setResult({ + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + }), + }); + + const simulateMutation = useMutation({ + mutationFn: async ({ + abiFunction, + callData, + value, + msgSender, + onSimulate, + }: SimulateParams) => { + const rawResult = await onSimulate({ + abiFunction, + callData, + value, + msgSender, + }); + const cleanResult = decodeResult(abiFunction, rawResult); + return { + type: "simulate" as const, + data: rawResult, + cleanResult: cleanResult || "Simulation successful", + }; + }, + onSuccess: (data) => setResult(data), + onError: (error) => + setResult({ + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + }), + }); + + const writeMutation = useMutation({ + mutationFn: async ({ + abiFunction, + callData, + value, + msgSender, + onWrite, + }: WriteParams) => { + const hash = await onWrite({ abiFunction, callData, value, msgSender }); + return { + type: "write" as const, + hash, + cleanResult: "Transaction submitted", + }; + }, + onSuccess: (data) => setResult(data), + onError: (error) => + setResult({ + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + }), + }); + + const isLoading = + readMutation.isPending || + simulateMutation.isPending || + writeMutation.isPending; + + return { + result, + isLoading, + read: readMutation.mutate, + simulate: simulateMutation.mutate, + write: writeMutation.mutate, + }; +} diff --git a/components/contract-execution/shared/hooks/use-execution-state.ts b/components/contract-execution/shared/hooks/use-execution-state.ts new file mode 100644 index 0000000..e707695 --- /dev/null +++ b/components/contract-execution/shared/hooks/use-execution-state.ts @@ -0,0 +1,86 @@ +import { useCallback, useState } from "react"; +import type { AbiFunction, Address, Hex } from "viem"; +import type { ExecutionParams } from "../../types.js"; +import { isWriteFunction } from "../utils/utils.js"; +import { useExecutionMutations } from "./use-execution-mutations.js"; + +export interface UseExecutionStateParams { + abiFunction?: AbiFunction | null; + defaultCallData?: Hex; + defaultValue?: bigint; + defaultSender?: Address; + onQuery?: (params: ExecutionParams) => Promise; + onWrite?: (params: ExecutionParams) => Promise; + onSimulate?: (params: ExecutionParams) => Promise; + msgSender?: Address; +} + +export function useExecutionState({ + abiFunction, + defaultCallData, + defaultValue, + onQuery, + onWrite, + onSimulate, + msgSender, +}: UseExecutionStateParams) { + const [callData, setCallData] = useState(defaultCallData); + const [value, setValue] = useState(defaultValue); + + const executionMutations = useExecutionMutations(); + + const handleCallDataChange = useCallback( + ({ data, value: newValue }: { data?: Hex; value?: bigint }) => { + setCallData(data); + setValue(newValue); + }, + [], + ); + + const isWrite = isWriteFunction(abiFunction); + + const handleSimulate = useCallback(() => { + if (!onSimulate || !callData) return; + executionMutations.simulate({ + abiFunction: abiFunction || undefined, + callData, + value, + msgSender, + onSimulate, + }); + }, [abiFunction, callData, value, msgSender, onSimulate, executionMutations]); + + const handleQuery = useCallback(() => { + if (!onQuery || !callData) return; + executionMutations.read({ + abiFunction: abiFunction || undefined, + callData, + value, + msgSender, + onQuery, + }); + }, [abiFunction, callData, value, msgSender, onQuery, executionMutations]); + + const handleWrite = useCallback(() => { + if (!callData || !onWrite) return; + executionMutations.write({ + abiFunction: abiFunction || undefined, + callData, + value, + msgSender, + onWrite, + }); + }, [abiFunction, callData, value, msgSender, onWrite, executionMutations]); + + return { + callData, + value, + handleCallDataChange, + isWrite, + result: executionMutations.result, + isLoading: executionMutations.isLoading, + handleSimulate, + handleQuery, + handleWrite, + }; +} diff --git a/components/contract-execution/shared/use-function-execution.ts b/components/contract-execution/shared/use-function-execution.ts deleted file mode 100644 index f794ed4..0000000 --- a/components/contract-execution/shared/use-function-execution.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { useCallback, useState } from "react"; -import type { AbiFunction, Address, Hex } from "viem"; -import { decodeFunctionResult } from "viem"; -import type { ExecutionParams } from "./types.js"; -import { formatDecodedResult } from "./utils.js"; - -export type InternalResult = { - type: "call" | "simulation" | "execution" | "error"; - data?: string; - hash?: Hex; - cleanResult?: string; - error?: string; -}; - -interface UseFunctionExecutionParams { - abiFunction: AbiFunction; - callData: string; - msgSender?: Address; - onQuery: (params: ExecutionParams) => Promise; - onWrite: (params: ExecutionParams) => Promise; - onSimulate?: (params: ExecutionParams) => Promise; -} - -export function useFunctionExecution() { - const [result, setResult] = useState(null); - const [isSimulating, setIsSimulating] = useState(false); - const [isExecuting, setIsExecuting] = useState(false); - - const simulate = useCallback( - async ({ - abiFunction, - callData, - msgSender, - onSimulate, - }: UseFunctionExecutionParams) => { - if (!callData || !onSimulate) return; - - const isWrite = - abiFunction.stateMutability !== "view" && - abiFunction.stateMutability !== "pure"; - - setIsSimulating(true); - try { - const rawResult = await onSimulate({ - abiFunction, - callData: callData as Hex, - msgSender, - }); - - if (isWrite) { - setResult({ - type: "simulation", - cleanResult: "Simulation successful", - data: rawResult, - }); - } else { - try { - const decoded = decodeFunctionResult({ - abi: [abiFunction], - functionName: abiFunction.name, - data: rawResult, - }); - - setResult({ - type: "simulation", - cleanResult: formatDecodedResult(decoded), - data: rawResult, - }); - } catch { - setResult({ - type: "simulation", - data: rawResult, - }); - } - } - } catch (error) { - setResult({ - type: "error", - error: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - setIsSimulating(false); - } - }, - [], - ); - - const execute = useCallback( - async ({ - abiFunction, - callData, - msgSender, - onQuery, - onWrite, - }: UseFunctionExecutionParams) => { - if (!callData) return; - - const isWrite = - abiFunction.stateMutability !== "view" && - abiFunction.stateMutability !== "pure"; - - setIsExecuting(true); - try { - if (isWrite) { - const hash = await onWrite({ - abiFunction, - callData: callData as Hex, - msgSender, - }); - - setResult({ - type: "execution", - hash, - cleanResult: "Transaction submitted", - }); - } else { - const rawResult = await onQuery({ - abiFunction, - callData: callData as Hex, - msgSender, - }); - - try { - const decoded = decodeFunctionResult({ - abi: [abiFunction], - functionName: abiFunction.name, - data: rawResult, - }); - - setResult({ - type: "call", - cleanResult: formatDecodedResult(decoded), - data: rawResult, - }); - } catch { - setResult({ - type: "call", - data: rawResult, - }); - } - } - } catch (error) { - setResult({ - type: "error", - error: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - setIsExecuting(false); - } - }, - [], - ); - - return { - result, - isSimulating, - isExecuting, - simulate, - execute, - }; -} diff --git a/components/contract-execution/shared/use-raw-execution.ts b/components/contract-execution/shared/use-raw-execution.ts deleted file mode 100644 index 7f8fe0a..0000000 --- a/components/contract-execution/shared/use-raw-execution.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useState } from "react"; -import type { Address, Hex } from "viem"; -import type { ExecutionParams } from "./types.js"; -import type { InternalResult } from "./use-function-execution.js"; - -interface UseRawExecutionParams { - isWrite: boolean; - onExecute: (params: ExecutionParams) => Promise; -} - -export function useRawExecution({ isWrite, onExecute }: UseRawExecutionParams) { - const [result, setResult] = useState(null); - const [isExecuting, setIsExecuting] = useState(false); - - const execute = async (params: { - callData: string; - value?: bigint; - msgSender?: Address; - }) => { - if (!params.callData) return; - - setIsExecuting(true); - try { - const response = await onExecute({ - callData: params.callData as Hex, - value: params.value, - msgSender: params.msgSender, - }); - - if (isWrite) { - setResult({ - type: "execution", - hash: response, - cleanResult: "Transaction submitted", - }); - } else { - setResult({ - type: "call", - data: response, - cleanResult: response, - }); - } - } catch (error) { - setResult({ - type: "error", - error: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - setIsExecuting(false); - } - }; - - return { - result, - isExecuting, - execute, - }; -} diff --git a/components/contract-execution/shared/form-utils.ts b/components/contract-execution/shared/utils/form-utils.ts similarity index 100% rename from components/contract-execution/shared/form-utils.ts rename to components/contract-execution/shared/utils/form-utils.ts diff --git a/components/contract-execution/shared/utils.ts b/components/contract-execution/shared/utils/utils.ts similarity index 100% rename from components/contract-execution/shared/utils.ts rename to components/contract-execution/shared/utils/utils.ts diff --git a/components/contract-execution/shared/types.ts b/components/contract-execution/types.ts similarity index 96% rename from components/contract-execution/shared/types.ts rename to components/contract-execution/types.ts index ba2e294..92d9b83 100644 --- a/components/contract-execution/shared/types.ts +++ b/components/contract-execution/types.ts @@ -1,5 +1,5 @@ import type { Abi, AbiFunction, Address, Hex } from "viem"; -import type { AddressData } from "../../address-autocomplete-input.js"; +import type { AddressData } from "../address-autocomplete-input.js"; export interface BaseExecutionProps { address: Address; diff --git a/stories/resend-transaction.stories.tsx b/stories/resend-transaction.stories.tsx index 7f68070..28f85db 100644 --- a/stories/resend-transaction.stories.tsx +++ b/stories/resend-transaction.stories.tsx @@ -1,10 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Abi } from "viem"; import { ResendTransaction } from "../components/contract-execution/resend-transaction/index.js"; -import type { - ExecutionParams, - RawCallParams, -} from "../components/contract-execution/shared/types.js"; +import type { ExecutionParams } from "../components/contract-execution/types.js"; const meta: Meta = { title: "ethui/ResendTransaction", @@ -57,13 +54,6 @@ const addresses = [ }, ]; -// Mock execution handlers -const mockQuery = async (params: ExecutionParams): Promise<`0x${string}`> => { - console.log("Query called with:", params); - await new Promise((resolve) => setTimeout(resolve, 1000)); - return "0x0000000000000000000000000000000000000000000000003635c9adc5dea00000"; -}; - const mockWrite = async (params: ExecutionParams): Promise<`0x${string}`> => { console.log("Write called with:", params); await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -78,14 +68,6 @@ const mockSimulate = async ( return "0x0000000000000000000000000000000000000000000000000000000000000001"; }; -const mockRawTransaction = async ( - params: RawCallParams, -): Promise<`0x${string}`> => { - console.log("Raw transaction with:", params); - await new Promise((resolve) => setTimeout(resolve, 1000)); - return "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; -}; - // Story: Resend with ABI (decoded function) export const WithAbi: Story = { args: { @@ -97,7 +79,6 @@ export const WithAbi: Story = { addresses, requiresConnection: true, isConnected: true, - onQuery: mockQuery, onWrite: mockWrite, onSimulate: mockSimulate, onHashClick: (hash: string) => { @@ -117,9 +98,7 @@ export const WithoutAbi: Story = { addresses, requiresConnection: true, isConnected: true, - onQuery: mockQuery, onWrite: mockWrite, - onRawTransaction: mockRawTransaction, onHashClick: (hash: string) => { console.log("Hash clicked:", hash); }, @@ -136,9 +115,7 @@ export const Disconnected: Story = { addresses, requiresConnection: true, isConnected: false, - onQuery: mockQuery, onWrite: mockWrite, onSimulate: mockSimulate, - onRawTransaction: mockRawTransaction, }, };