diff --git a/components/contract-execution/components/shared-components.tsx b/components/contract-execution/components/shared-components.tsx deleted file mode 100644 index 46a64ff..0000000 --- a/components/contract-execution/components/shared-components.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Info } from "lucide-react"; -import { Form } from "../../form/index.js"; -import { Alert, AlertDescription, AlertTitle } from "../../shadcn/alert.js"; -import { Button } from "../../shadcn/button.js"; - -export function MsgSenderInput() { - return ( -
- Msg Sender (Optional) - } - placeholder="0x..." - className="w-full" - /> -
- ); -} - -export function ConnectWalletAlert() { - return ( - - - Connect your wallet - - Connect your wallet to execute this write function - - - ); -} - -interface ActionButtonsProps { - isWrite: boolean; - callData: string | undefined; - isSimulating: boolean; - isExecuting: boolean; - isConnected: boolean; - hasSimulate: boolean; - simulate?: () => void; - execute: () => void; -} - -export function ActionButtons({ - isWrite, - callData, - isSimulating, - isExecuting, - isConnected, - hasSimulate, - simulate, - execute, -}: ActionButtonsProps) { - return ( -
- {isWrite ? ( - <> - {hasSimulate && ( - - )} - - - ) : ( - - )} -
- ); -} diff --git a/components/contract-execution/components/function-item.tsx b/components/contract-execution/contract-execution-tabs/function-item.tsx similarity index 71% rename from components/contract-execution/components/function-item.tsx rename to components/contract-execution/contract-execution-tabs/function-item.tsx index f3281b6..75be755 100644 --- a/components/contract-execution/components/function-item.tsx +++ b/components/contract-execution/contract-execution-tabs/function-item.tsx @@ -1,52 +1,29 @@ -import { zodResolver } from "@hookform/resolvers/zod"; import { memo, useCallback, useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import type { AbiFunction, Address } from "viem"; -import { isAddress } from "viem"; -import { z } from "zod"; +import { FormProvider } from "react-hook-form"; +import type { AbiFunction } from "viem"; import { AbiItemFormWithPreview } from "../../abi-form/abi-item-form-with-preview.js"; -import type { AddressData } from "../../address-autocomplete-input.js"; import { AccordionContent, AccordionItem, AccordionTrigger, } from "../../shadcn/accordion.js"; -import type { ExecutionParams } from "../types.js"; -import { useFunctionExecution } from "../use-function-execution.js"; -import { DefaultResultDisplay } from "./result-display.js"; import { ActionButtons, ConnectWalletAlert, + DefaultResultDisplay, MsgSenderInput, -} from "./shared-components.js"; +} 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"; -const executionFormSchema = z.object({ - msgSender: z - .string() - .refine( - (val) => { - if (!val) return true; - return isAddress(val); - }, - { message: "Invalid address format" }, - ) - .optional(), -}); - -interface FunctionItemProps { +interface FunctionItemProps extends BaseExecutionProps { func: AbiFunction; index: number; - address: Address; - chainId: number; - sender?: Address; - addresses?: AddressData[]; - requiresConnection: boolean; - isConnected: boolean; onQuery: (params: ExecutionParams) => Promise<`0x${string}`>; onWrite: (params: ExecutionParams) => Promise<`0x${string}`>; onSimulate?: (params: ExecutionParams) => Promise<`0x${string}`>; - addressRenderer?: (address: Address) => React.ReactNode; - onHashClick?: (hash: string) => void; } export const FunctionItem = memo( @@ -68,19 +45,9 @@ export const FunctionItem = memo( const [callData, setCallData] = useState(""); const { result, isSimulating, isExecuting, simulate, execute } = useFunctionExecution(); + const { form, msgSender } = useMsgSenderForm(sender); - const form = useForm({ - mode: "onChange", - resolver: zodResolver(executionFormSchema), - defaultValues: { - msgSender: "", - }, - }); - - const msgSender = form.watch().msgSender || ""; - - const isWrite = - func.stateMutability !== "view" && func.stateMutability !== "pure"; + const isWrite = isWriteFunction(func); const handleCallDataChange = useCallback( (newCallData: string | undefined) => { @@ -93,7 +60,7 @@ export const FunctionItem = memo( simulate({ abiFunction: func, callData, - msgSender: msgSender ? (msgSender as Address) : undefined, + msgSender, onQuery, onWrite, onSimulate, @@ -104,7 +71,7 @@ export const FunctionItem = memo( execute({ abiFunction: func, callData, - msgSender: msgSender ? (msgSender as Address) : undefined, + msgSender, onQuery, onWrite, onSimulate, @@ -128,10 +95,6 @@ export const FunctionItem = memo(
{isWrite && } - {isWrite && requiresConnection && !isConnected && ( - - )} - + {isWrite && requiresConnection && !isConnected && ( + + )} + { - if (!val) return true; - return isAddress(val); - }, - { message: "Invalid address format" }, - ) - .optional(), -}); - -interface RawOperationsProps { - address: Address; - chainId: number; - sender?: Address; - addresses?: AddressData[]; - requiresConnection: boolean; - isConnected: boolean; +import { + ConnectWalletAlert, + DefaultResultDisplay, + MsgSenderInput, +} from "../shared/components.js"; +import { useMsgSenderForm } from "../shared/form-utils.js"; +import type { BaseExecutionProps, RawCallParams } from "../shared/types.js"; +import { useRawExecution } from "../shared/use-raw-execution.js"; + +interface RawOperationsProps extends BaseExecutionProps { onRawCall?: (params: RawCallParams) => Promise<`0x${string}`>; onRawTransaction?: (params: RawCallParams) => Promise<`0x${string}`>; - addressRenderer?: (address: Address) => React.ReactNode; - onHashClick?: (hash: string) => void; } export function RawOperations({ @@ -99,17 +71,9 @@ export function RawOperations({ ); } -interface RawOperationItemProps { +interface RawOperationItemProps extends BaseExecutionProps { type: "call" | "transaction"; - address: Address; - chainId: number; - sender?: Address; - addresses?: AddressData[]; - requiresConnection: boolean; - isConnected: boolean; onExecute: (params: RawCallParams) => Promise<`0x${string}`>; - addressRenderer?: (address: Address) => React.ReactNode; - onHashClick?: (hash: string) => void; } function RawOperationItem({ @@ -126,20 +90,17 @@ function RawOperationItem({ }: RawOperationItemProps) { const [callData, setCallData] = useState(""); const [value, setValue] = useState(); - const [result, setResult] = useState(null); - const [isExecuting, setIsExecuting] = useState(false); - - const form = useForm({ - mode: "onChange", - resolver: zodResolver(executionFormSchema), - defaultValues: { - msgSender: "", - }, - }); - - const msgSender = form.watch().msgSender || ""; + 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" @@ -147,44 +108,15 @@ function RawOperationItem({ : "Send transaction with arbitrary calldata"; const handleCallDataChange = useCallback( - ({ data, value: newValue }: { data?: `0x${string}`; value?: bigint }) => { + ({ data, value: newValue }: { data?: Hex; value?: bigint }) => { setCallData(data || ""); setValue(newValue); }, [], ); - const handleExecute = async () => { - if (!callData) return; - setIsExecuting(true); - try { - const result = await onExecute({ - data: callData as `0x${string}`, - value, - msgSender: msgSender ? (msgSender as Address) : undefined, - }); - - if (isWrite) { - setResult({ - type: "execution", - hash: result, - cleanResult: "Transaction submitted", - }); - } else { - setResult({ - type: "call", - data: result, - cleanResult: result, - }); - } - } catch (error) { - setResult({ - type: "error", - error: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - setIsExecuting(false); - } + const handleExecute = () => { + executeRaw({ callData, value, msgSender }); }; return ( @@ -203,10 +135,6 @@ function RawOperationItem({
{isWrite && } - {isWrite && requiresConnection && !isConnected && ( - - )} - + {isWrite && requiresConnection && !isConnected && ( + + )} +
+ )} + + + ) : ( + + )} +
+ ); +} interface DefaultResultDisplayProps { result: InternalResult; diff --git a/components/contract-execution/shared/form-utils.ts b/components/contract-execution/shared/form-utils.ts new file mode 100644 index 0000000..c37bc65 --- /dev/null +++ b/components/contract-execution/shared/form-utils.ts @@ -0,0 +1,40 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import type { Address } from "viem"; +import { isAddress } from "viem"; +import { z } from "zod"; + +export const msgSenderSchema = z.object({ + msgSender: z + .string() + .refine( + (val) => { + if (!val) return true; + return isAddress(val); + }, + { message: "Invalid address format" }, + ) + .optional(), +}); + +export type MsgSenderFormData = z.infer; + +export function useMsgSenderForm(defaultSender?: Address) { + const form = useForm({ + mode: "onChange", + resolver: zodResolver(msgSenderSchema), + defaultValues: { + msgSender: defaultSender || "", + }, + }); + + const msgSenderValue = form.watch("msgSender"); + const msgSender: Address | undefined = msgSenderValue + ? (msgSenderValue as Address) + : undefined; + + return { + form, + msgSender, + }; +} diff --git a/components/contract-execution/types.ts b/components/contract-execution/shared/types.ts similarity index 81% rename from components/contract-execution/types.ts rename to components/contract-execution/shared/types.ts index 96fe822..8c962b2 100644 --- a/components/contract-execution/types.ts +++ b/components/contract-execution/shared/types.ts @@ -1,15 +1,26 @@ -import type { Abi, AbiFunction, Address } from "viem"; -import type { AddressData } from "../address-autocomplete-input.js"; +import type { Abi, AbiFunction, Address, Hex } from "viem"; +import type { AddressData } from "../../address-autocomplete-input.js"; + +export interface BaseExecutionProps { + address: Address; + chainId: number; + sender?: Address; + addresses?: AddressData[]; + requiresConnection: boolean; + isConnected: boolean; + addressRenderer?: (address: Address) => React.ReactNode; + onHashClick?: (hash: string) => void; +} export interface ExecutionParams { abiFunction: AbiFunction; - callData: `0x${string}`; + callData: Hex; msgSender?: Address; value?: bigint; } export interface RawCallParams { - data: `0x${string}`; + data: Hex; value?: bigint; msgSender?: Address; } diff --git a/components/contract-execution/use-function-execution.ts b/components/contract-execution/shared/use-function-execution.ts similarity index 89% rename from components/contract-execution/use-function-execution.ts rename to components/contract-execution/shared/use-function-execution.ts index 7e8f255..f794ed4 100644 --- a/components/contract-execution/use-function-execution.ts +++ b/components/contract-execution/shared/use-function-execution.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import type { AbiFunction, Address } from "viem"; +import type { AbiFunction, Address, Hex } from "viem"; import { decodeFunctionResult } from "viem"; import type { ExecutionParams } from "./types.js"; import { formatDecodedResult } from "./utils.js"; @@ -7,7 +7,7 @@ import { formatDecodedResult } from "./utils.js"; export type InternalResult = { type: "call" | "simulation" | "execution" | "error"; data?: string; - hash?: string; + hash?: Hex; cleanResult?: string; error?: string; }; @@ -16,9 +16,9 @@ interface UseFunctionExecutionParams { abiFunction: AbiFunction; callData: string; msgSender?: Address; - onQuery: (params: ExecutionParams) => Promise<`0x${string}`>; - onWrite: (params: ExecutionParams) => Promise<`0x${string}`>; - onSimulate?: (params: ExecutionParams) => Promise<`0x${string}`>; + onQuery: (params: ExecutionParams) => Promise; + onWrite: (params: ExecutionParams) => Promise; + onSimulate?: (params: ExecutionParams) => Promise; } export function useFunctionExecution() { @@ -43,7 +43,7 @@ export function useFunctionExecution() { try { const rawResult = await onSimulate({ abiFunction, - callData: callData as `0x${string}`, + callData: callData as Hex, msgSender, }); @@ -104,7 +104,7 @@ export function useFunctionExecution() { if (isWrite) { const hash = await onWrite({ abiFunction, - callData: callData as `0x${string}`, + callData: callData as Hex, msgSender, }); @@ -116,7 +116,7 @@ export function useFunctionExecution() { } else { const rawResult = await onQuery({ abiFunction, - callData: callData as `0x${string}`, + callData: callData as Hex, msgSender, }); diff --git a/components/contract-execution/shared/use-raw-execution.ts b/components/contract-execution/shared/use-raw-execution.ts new file mode 100644 index 0000000..82d6332 --- /dev/null +++ b/components/contract-execution/shared/use-raw-execution.ts @@ -0,0 +1,58 @@ +import { useState } from "react"; +import type { Address, Hex } from "viem"; +import type { RawCallParams } from "./types.js"; +import type { InternalResult } from "./use-function-execution.js"; + +interface UseRawExecutionParams { + isWrite: boolean; + onExecute: (params: RawCallParams) => 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({ + data: 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/utils.ts b/components/contract-execution/shared/utils.ts similarity index 68% rename from components/contract-execution/utils.ts rename to components/contract-execution/shared/utils.ts index f8f327a..6f4f18b 100644 --- a/components/contract-execution/utils.ts +++ b/components/contract-execution/shared/utils.ts @@ -1,3 +1,5 @@ +import type { AbiFunction } from "viem"; + export function formatDecodedResult(result: unknown): string { if (typeof result === "bigint") { return result.toString(); @@ -18,3 +20,8 @@ export function formatDecodedResult(result: unknown): string { } return String(result); } + +export function isWriteFunction(func: AbiFunction | null | undefined): boolean { + if (!func) return true; + return func.stateMutability !== "view" && func.stateMutability !== "pure"; +} diff --git a/stories/contract-functions-list.stories.tsx b/stories/contract-functions-list.stories.tsx index 6db2aa3..25924f1 100644 --- a/stories/contract-functions-list.stories.tsx +++ b/stories/contract-functions-list.stories.tsx @@ -1,11 +1,11 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import React, { useState } from "react"; import type { Abi } from "viem"; -import { - ContractFunctionsList, - type ExecutionParams, - type RawCallParams, -} from "../components/contract-execution/index.js"; +import { ContractFunctionsList } from "../components/contract-execution/contract-execution-tabs/index.js"; +import type { + ExecutionParams, + RawCallParams, +} from "../components/contract-execution/shared/types.js"; import { Button } from "../components/shadcn/button.js"; const meta: Meta = { diff --git a/stories/raw-operations.stories.tsx b/stories/raw-operations.stories.tsx deleted file mode 100644 index c087e03..0000000 --- a/stories/raw-operations.stories.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import React from "react"; -import { - type RawCallParams, - RawOperations, -} from "../components/contract-execution/index.js"; - -const meta: Meta = { - title: "ethui/RawOperations", - component: RawOperations, - parameters: { - layout: "padded", - }, - tags: ["autodocs"], - decorators: [ - (Story) => ( -
- -
- ), - ], -}; - -export default meta; -type Story = StoryObj; - -// Mock addresses for autocomplete -const addresses = [ - { - address: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - alias: "Test Wallet 1", - wallet: "MetaMask", - }, - { - address: "0x0d21F3BCF7e003A825a8c6EE698EAFaB9d3CC82a", - alias: "Test Wallet 2", - wallet: "Coinbase", - }, -]; - -// Mock raw operation handlers -const mockRawCall = async (params: RawCallParams): Promise<`0x${string}`> => { - console.log("Raw call executed:", params); - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Return mock raw hex result (encoded uint256: 42) - return "0x000000000000000000000000000000000000000000000000000000000000002a"; -}; - -const mockRawTransaction = async ( - params: RawCallParams, -): Promise<`0x${string}`> => { - console.log("Raw transaction executed:", params); - await new Promise((resolve) => setTimeout(resolve, 1500)); - - // Return mock transaction hash - return "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; -}; - -// Story: Both operations available -export const BothOperations: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - addresses, - requiresConnection: true, - isConnected: true, - onRawCall: mockRawCall, - onRawTransaction: mockRawTransaction, - }, -}; - -// Story: Only raw call available -export const OnlyRawCall: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - addresses, - requiresConnection: true, - isConnected: true, - onRawCall: mockRawCall, - // No onRawTransaction - }, -}; - -// Story: Only raw transaction available -export const OnlyRawTransaction: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - addresses, - requiresConnection: true, - isConnected: true, - // No onRawCall - onRawTransaction: mockRawTransaction, - }, -}; - -// Story: Disconnected wallet (shows connection alert for raw transaction) -export const Disconnected: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - addresses, - requiresConnection: true, - isConnected: false, - onRawCall: mockRawCall, - onRawTransaction: mockRawTransaction, - }, -}; - -// Story: With custom address renderer -const customAddressRenderer = (address: string) => { - return ( - - 🔧 {address.slice(0, 6)}...{address.slice(-4)} - - ); -}; - -export const CustomAddressRenderer: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - addresses, - requiresConnection: true, - isConnected: true, - onRawCall: mockRawCall, - onRawTransaction: mockRawTransaction, - addressRenderer: customAddressRenderer, - }, -}; - -// Story: With error handling -const mockRawTransactionWithError = async ( - _params: RawCallParams, -): Promise<`0x${string}`> => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - throw new Error("Transaction reverted: insufficient gas"); -}; - -export const WithError: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - addresses, - requiresConnection: true, - isConnected: true, - onRawCall: mockRawCall, - onRawTransaction: mockRawTransactionWithError, - }, -}; - -// Story: With hash click handler -export const WithHashClick: Story = { - args: { - address: "0x1234567890123456789012345678901234567890", - chainId: 1, - sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", - addresses, - requiresConnection: true, - isConnected: true, - onRawCall: mockRawCall, - onRawTransaction: mockRawTransaction, - onHashClick: (hash) => { - console.log("Hash clicked:", hash); - alert(`Opening explorer for: ${hash}`); - window.open(`https://etherscan.io/tx/${hash}`, "_blank"); - }, - }, -}; diff --git a/stories/resend-transaction.stories.tsx b/stories/resend-transaction.stories.tsx new file mode 100644 index 0000000..7f68070 --- /dev/null +++ b/stories/resend-transaction.stories.tsx @@ -0,0 +1,144 @@ +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"; + +const meta: Meta = { + title: "ethui/ResendTransaction", + component: ResendTransaction, + parameters: { + layout: "padded", + }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +// Mock ERC20 ABI +const mockERC20Abi = [ + { + type: "function", + name: "transfer", + inputs: [ + { name: "to", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "success", type: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "balanceOf", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "balance", type: "uint256" }], + stateMutability: "view", + }, +] as const satisfies Abi; + +// Mock encoded transfer call +const mockTransferCalldata = + "0xa9059cbb0000000000000000000000001234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000000a" as const; + +// Mock addresses for autocomplete +const addresses = [ + { + address: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", + alias: "Test Wallet 1", + wallet: "MetaMask", + }, + { + address: "0x0d21F3BCF7e003A825a8c6EE698EAFaB9d3CC82a", + alias: "Test Wallet 2", + wallet: "Coinbase", + }, +]; + +// 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)); + return "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; +}; + +const mockSimulate = async ( + params: ExecutionParams, +): Promise<`0x${string}`> => { + console.log("Simulate called with:", params); + await new Promise((resolve) => setTimeout(resolve, 800)); + 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: { + to: "0x1234567890123456789012345678901234567890", + input: mockTransferCalldata, + abi: mockERC20Abi, + chainId: 1, + sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", + addresses, + requiresConnection: true, + isConnected: true, + onQuery: mockQuery, + onWrite: mockWrite, + onSimulate: mockSimulate, + onHashClick: (hash: string) => { + console.log("Hash clicked:", hash); + window.open(`https://etherscan.io/tx/${hash}`, "_blank"); + }, + }, +}; + +// Story: Resend without ABI (raw transaction) +export const WithoutAbi: Story = { + args: { + to: "0x1234567890123456789012345678901234567890", + input: mockTransferCalldata, + chainId: 1, + sender: "0x0077014b4C74d9b1688847386B24Ed23Fdf14Be8", + addresses, + requiresConnection: true, + isConnected: true, + onQuery: mockQuery, + onWrite: mockWrite, + onRawTransaction: mockRawTransaction, + onHashClick: (hash: string) => { + console.log("Hash clicked:", hash); + }, + }, +}; + +// Story: Disconnected wallet +export const Disconnected: Story = { + args: { + to: "0x1234567890123456789012345678901234567890", + input: mockTransferCalldata, + abi: mockERC20Abi, + chainId: 1, + addresses, + requiresConnection: true, + isConnected: false, + onQuery: mockQuery, + onWrite: mockWrite, + onSimulate: mockSimulate, + onRawTransaction: mockRawTransaction, + }, +};