- {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,
},
};