Skip to main content
Send transactions through your EIP-4337 smart account using UserOperations.

Prerequisites

Setup Smart Account Client

Send User Operations

useSendUserOperation.ts
import { useState, useCallback } from "react";
import type { ModularAccountV2Client } from "@account-kit/smart-contracts";
import type { Address, Hash, Hex } from "viem";

interface UserOperationCall {
  target: Address;
  data?: Hex;
  value?: bigint;
}

export function useSendUserOperation(client: ModularAccountV2Client | null) {
  const [isPending, setIsPending] = useState(false);
  const [txHash, setTxHash] = useState<Hash | null>(null);
  const [error, setError] = useState<Error | null>(null);

  const sendUserOperation = useCallback(
    async (calls: UserOperationCall | UserOperationCall[]): Promise<Hash> => {
      if (!client) throw new Error("Client not initialized");

      setIsPending(true);
      setError(null);

      try {
        const callsArray = Array.isArray(calls) ? calls : [calls];

        const formattedCalls = callsArray.map((call) => ({
          target: call.target,
          data: call.data ?? "0x",
          value: call.value ?? BigInt(0),
        }));

        const userOpHash = await client.sendUserOperation({
          uo: formattedCalls.length === 1 ? formattedCalls[0] : formattedCalls,
        });

        const hash = await client.waitForUserOperationTransaction(userOpHash);
        setTxHash(hash);
        return hash;
      } catch (err) {
        const error = err instanceof Error ? err : new Error("Failed to send");
        setError(error);
        throw error;
      } finally {
        setIsPending(false);
      }
    },
    [client]
  );

  return { sendUserOperation, isPending, txHash, error };
}

Usage

import { useSmartAccountClient } from "./useSmartAccountClient";
import { useSendUserOperation } from "./useSendUserOperation";
import { parseEther } from "viem";

function SendTransaction() {
  const { client, address } = useSmartAccountClient();
  const { sendUserOperation, isPending, txHash } = useSendUserOperation(client);

  const handleSend = async () => {
    const hash = await sendUserOperation({
      target: "0x...",
      value: parseEther("0.001"),
    });
    console.log("Transaction:", hash);
  };

  return (
    <div>
      <p>Smart Account: {address}</p>
      <button onClick={handleSend} disabled={isPending}>
        {isPending ? "Sending..." : "Send 0.001 ETH"}
      </button>
      {txHash && <p>Tx: {txHash}</p>}
    </div>
  );
}

Batched Transactions

Most 4337 providers support batching multiple calls into a single UserOperation:
const hash = await sendUserOperation([
  { target: "0xToken...", data: approveCalldata },
  { target: "0xDex...", data: swapCalldata },
]);

Next Steps