Skip to main content

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.
ParameterTypeDescription
callsBatchCall[]Array of calls to execute (single or multiple)
calls[].toHexThe recipient address for this call
calls[].valuebigintThe value to send in wei for this call
calls[].dataHex (optional)The transaction data for this call
walletAccountEvmWalletAccount (optional)The wallet account. Required if no kernelClient is provided
kernelClientKernelClient (optional)An existing kernel client. Required if no walletAccount is provided
withSponsorshipboolean (optional)Whether to use sponsorship. Default: true. Only used with walletAccount

Returns

Promise<UserOperationReceipt> - Returns the UserOperation receipt with transaction details. The receipt includes:
  • userOpHash: The UserOperation hash
  • success: Boolean indicating if the operation succeeded
  • Additional transaction details from the receipt

Examples

Single transaction (sponsored by default)

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

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

Batch transaction (multiple recipients)

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)

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

Using existing kernel client

const kernelClient = await createKernelClientForWalletAccount({
  smartWalletAccount: walletAccount,
});

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

Token transfer

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 { 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";

const calls = [
  { to: "0x...", value: parseEther("0.1") },
  { to: "0x...", 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

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);
}

Benefits of Batch Transactions

  • Atomicity: All operations succeed or all fail together
  • Cost Efficiency: Single validation per batch reduces gas fees
  • Better UX: Users approve once for multiple operations
  • Composability: Enable complex multi-step workflows

Notes

  • By default, transactions use sponsorship (withSponsorship: true)
  • Batch transactions are atomic - if any call fails, the entire batch fails
  • The function waits for the UserOperation receipt before returning
  • Gas savings increase with larger batches due to shared validation costs