send user operation.md

sendUserOperation

Sends a user operation with one or more calls and waits for the transaction receipt. Works for both single transactions and batch transactions. This is the primary function for executing transactions with ZeroDev Account Abstraction.

Usage

import { sendUserOperation } from "@dynamic-labs-sdk/zerodev";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { getPrimaryWalletAccount } from "@dynamic-labs-sdk/client";
import { parseEther } from "viem";

const walletAccount = getPrimaryWalletAccount();

if (walletAccount && isEvmWalletAccount(walletAccount)) {
  const receipt = await sendUserOperation({
    walletAccount,
    calls: [
      {
        to: recipientAddress,
        value: parseEther("0.01"),
        data: "0x",
      },
    ],
  });

console.log("Transaction successful:", receipt.userOpHash);
  console.log("Success:", receipt.success);
}

Parameters

You must provide either a walletAccount or a kernelClient, but not both.

Parameter Type Description
calls BatchCall[] Array of calls to execute (single or multiple)
calls[].to Hex The recipient address for this call
calls[].value bigint The value to send in wei for this call
calls[].data Hex (optional) The transaction data for this call
walletAccount EvmWalletAccount (optional) The wallet account. Required if no kernelClient is provided
kernelClient KernelClient (optional) An existing kernel client. Required if no walletAccount is provided
withSponsorship boolean (optional) Whether to use sponsorship. Default: true. Only used with walletAccount

Returns

Promise<UserOperationReceipt> - Returns the UserOperation receipt with transaction details.

The receipt includes:

Examples

Single transaction (sponsored by default)

import { sendUserOperation } from '@dynamic-labs-sdk/zerodev';
const receipt = await sendUserOperation({
  walletAccount,
  calls: [
    {
      to: "0x...",
      value: parseEther("0.1"),
    },
  ],
});

console.log("UserOp Hash:", receipt.userOpHash);

Batch transaction (multiple recipients)

import { sendUserOperation } from '@dynamic-labs-sdk/zerodev';
const receipt = await sendUserOperation({
  walletAccount,
  calls: [
    { to: "0xRecipient1...", value: parseEther("0.1") },
    { to: "0xRecipient2...", value: parseEther("0.2") },
    { to: "0xRecipient3...", value: parseEther("0.3") },
  ],
});

console.log("Batch transaction successful:", receipt.userOpHash);

Without sponsorship (user pays gas)

import { sendUserOperation } from '@dynamic-labs-sdk/zerodev';
const receipt = await sendUserOperation({
  walletAccount,
  withSponsorship: false,
  calls: [
    {
      to: "0x...",
      value: parseEther("0.1"),
    },
  ],
});

Using existing kernel client

import { createKernelClientForWalletAccount, sendUserOperation } from '@dynamic-labs-sdk/zerodev';
const kernelClient = await createKernelClientForWalletAccount({
  smartWalletAccount: walletAccount,
});

const receipt = await sendUserOperation({
  kernelClient,
  calls: [
    {
      to: "0x...",
      value: parseEther("0.1"),
    },
  ],
});

Token transfer

import { sendUserOperation } from "@dynamic-labs-sdk/zerodev";
import { encodeFunctionData } from "viem";

const receipt = await sendUserOperation({
  walletAccount,
  calls: [
    {
      to: tokenAddress,
      value: 0n,
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "transfer",
        args: [recipientAddress, parseEther("100")],
      }),
    },
  ],
});

DeFi workflow (Approve + Swap)

import { sendUserOperation } from "@dynamic-labs-sdk/zerodev";
import { encodeFunctionData } from "viem";

const receipt = await sendUserOperation({
  walletAccount,
  calls: [
    {
      to: tokenAddress,
      value: 0n,
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "approve",
        args: [swapRouterAddress, amount],
      }),
    },
    {
      to: swapRouterAddress,
      value: 0n,
      data: encodeFunctionData({
        abi: swapRouterAbi,
        functionName: "swap",
        args: [tokenA, tokenB, amount],
      }),
    },
  ],
});

console.log("Approve + Swap completed atomically:", receipt.userOpHash);

With gas estimation and sponsorship check

import { estimateUserOperationGas, canSponsorUserOperation, sendUserOperation } from "@dynamic-labs-sdk/zerodev";
import type { BatchCall } from "@dynamic-labs-sdk/zerodev";

const calls: BatchCall[] = [
  { to: "0x742d35Cc6634C0532925a3b844Bc9e7595f7ABCD", value: parseEther("0.1") },
  { to: "0x8ba1f109551bD432803012645Ac136ddd64DBA72", value: parseEther("0.2") },
];

// Estimate gas
const estimatedGas = await estimateUserOperationGas({
  walletAccount,
  calls,
});

// Check sponsorship
const canSponsor = await canSponsorUserOperation({
  walletAccount,
  calls,
});

console.log(`Estimated gas: ${formatEther(estimatedGas)} ETH`);
console.log(`Will be sponsored: ${canSponsor}`);

// Send with appropriate sponsorship setting
const receipt = await sendUserOperation({
  walletAccount,
  withSponsorship: canSponsor,
  calls,
});

Error handling

import { sendUserOperation } from '@dynamic-labs-sdk/zerodev';
try {
  const receipt = await sendUserOperation({
    walletAccount,
    calls: [
      { to: "0xRecipient1...", value: parseEther("0.1") },
      { to: "0xRecipient2...", value: parseEther("0.2") },
    ],
  });

console.log("All operations succeeded:", receipt.userOpHash);
} catch (error) {
  console.error("Batch transaction failed - no operations were executed");
  console.error(error);
}

React

Use sendUserOperation inside a button handler. Use useGetWalletAccounts from @dynamic-labs-sdk/react-hooks to reactively get the wallet account:

import { sendUserOperation } from '@dynamic-labs-sdk/zerodev';
import { isEvmWalletAccount } from '@dynamic-labs-sdk/evm';
import { useGetWalletAccounts } from '@dynamic-labs-sdk/react-hooks';
import { useState } from 'react';
import { parseEther } from 'viem';

function GaslessSendButton({ recipientAddress }) {
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const walletAccount = walletAccounts.find(isEvmWalletAccount);
  const [opHash, setOpHash] = useState('');

const handleSend = async () => {
    if (!walletAccount) return;
    const receipt = await sendUserOperation({
      walletAccount,
      calls: [{ to: recipientAddress, value: parseEther('0.01'), data: '0x' }],
    });
    setOpHash(receipt.userOpHash);
  };

return (
    <div>
      <button onClick={handleSend} disabled={!walletAccount}>Send (Gasless)</button>
      {opHash && <p>UserOp hash: {opHash}</p>}
    </div>
  );
}

Benefits of Batch Transactions

Notes