Overview

Smart accounts are a type of account that can be used to execute user operations onchain.

In this guide, you will learn how to:

  • Create an EVM smart account
  • Send a user operation from the smart account
  • Batch calls within a single user operation

Smart accounts currently have the following limitations:

  • An EVM account can be the owner of only one smart account.
  • A smart account can only have one owner.
  • User operations must be sent sequentially, not concurrently. A single user operation can contain multiple calls.

Prerequisites

It is assumed you have already completed the Quickstart guide.

1. Create a smart account

An EVM smart account is a smart contract account deployed on an EVM compatible network that provides the ability to batch transactions, sponsor gas, and manage spend permissions.

Smart accounts require an owner account to sign on its behalf.

In this example, we will only create the smart account, and use a CDP EVM account as the owner. Note that the smart contract is not deployed until the following step when you submit the first user operation.

Smart accounts are created with the CREATE2 opcode, allowing us to access a contract address before it is deployed.

The actual smart contract is not deployed until the first user operation is submitted.

Learn more about account abstraction in the Coinbase Crypto glossary.

    import { CdpClient } from "@coinbase/cdp-sdk";
    import dotenv from "dotenv";

    dotenv.config();

    const cdp = new CdpClient();

    const account = await cdp.evm.createAccount();

    const smartAccount = await cdp.evm.createSmartAccount({
      owner: account,
    });

    console.log(
      `Created smart account: ${smartAccount.address}. Owner address: ${account.address}`
    );

After running the above snippet, you should see output similar to the following:

Created smart account: 0x7a3D84055994c3062819Ce8730869D0aDeA4c3Bf

2. Send a user operation

A user operation is a transaction that is executed by a smart account. In this example, we will:

  • Create an externally owned account
  • Create a smart account with the EOA as the owner
  • Submit a user operation on Base Sepolia from the smart account which transfers 0 ETH to the EOA

On Base Sepolia, smart account user operations are subsidized, meaning the smart account does not need to be funded with ETH to submit a user operation. On Base mainnet, you will need to fund the smart account with ETH before you can submit a user operation.

    import { CdpClient } from "@coinbase/cdp-sdk";

    import { parseEther } from "viem";

    import dotenv from "dotenv";

    dotenv.config();

    const cdp = new CdpClient();

    const owner = await cdp.evm.createAccount({});
    console.log("Created owner account:", owner.address);

    const smartAccount = await cdp.evm.createSmartAccount({
      owner,
    });
    console.log("Created smart account:", smartAccount.address);

    const result = await cdp.evm.sendUserOperation({
      smartAccount,
      network: "base-sepolia",
      calls: [
        {
          to: "0x0000000000000000000000000000000000000000",
          value: parseEther("0"),
          data: "0x",
        },
      ],
    });

    console.log("User operation status:", result.status);

    console.log("Waiting for user operation to be confirmed...");
    const userOperation = await cdp.evm.waitForUserOperation({
      smartAccountAddress: smartAccount.address,
      userOpHash: result.userOpHash,
    });

    if (userOperation.status === "complete") {
      console.log("User operation confirmed. Block explorer link:", `https://sepolia.basescan.org/tx/${userOperation.transactionHash}`);
    } else {
      console.log("User operation failed");
    }

After running the above snippet, you should see similar output:

Created owner account: 0x088a49cAf927B8DacEFc4ccFD0D5EAdeC06F19A2
Created smart account: 0x929444AFfd714c260bb6695c921bEB99d1D31ff7
User operation status: broadcast
Waiting for user operation to be confirmed...
User operation confirmed. Block explorer link: https://basescan.org/tx/0x8e66c974c8d1b2a75fee35e097fe9171d28c48066472bb6ed81ca81a10d3c321```

3. Batch calls within a single user operation

A smart account can batch multiple calls in a single user operation through the calls field.

In this example, we will:

  • Create an externally owned account
  • Create a smart account with the EOA as the owner
  • Fund the smart account using a faucet
  • Submit a batch transaction with 3 calls
    import { CdpClient } from "@coinbase/cdp-sdk";

    import { createPublicClient, http, parseEther, Calls } from "viem";
    import { baseSepolia } from "viem/chains";
    import dotenv from "dotenv";

    dotenv.config();

    const cdp = new CdpClient();

    const account = await cdp.evm.createAccount();
    const smartAccount = await cdp.evm.createSmartAccount({ owner: account });
    console.log("Created smart account:", smartAccount.address);

    const { transactionHash } = await smartAccount.requestFaucet({
      network: "base-sepolia",
      token: "eth",
    });

    const publicClient = createPublicClient({
      chain: baseSepolia,
      transport: http(),
    });

    const faucetTxReceipt = await publicClient.waitForTransactionReceipt({
      hash: transactionHash,
    });
    console.log("Faucet transaction confirmed:", faucetTxReceipt.transactionHash);

    const destinationAddresses = [
      "0xba5f3764f0A714EfaEDC00a5297715Fd75A416B7",
      "0xD84523e4F239190E9553ea59D7e109461752EC3E",
      "0xf1F7Bf05A81dBd5ACBc701c04ce79FbC82fEAD8b",
    ];

    const calls = destinationAddresses.map((destinationAddress) => ({
      to: destinationAddress,
      value: parseEther("0.000001"),
      data: "0x",
    }));

    console.log("Sending user operation to three destinations...");
    const { userOpHash } = await smartAccount.sendUserOperation({
      network: "base-sepolia",
      calls: calls as Calls<unknown[]>,
    });

    console.log("Waiting for user operation to be confirmed...");
    const userOperationResult = await smartAccount.waitForUserOperation({
      userOpHash,
    });

    if (userOperationResult.status === "complete") {
      console.log("User operation confirmed. Block explorer link:", `https://sepolia.basescan.org/tx/${userOperationResult.transactionHash}`);
    } else {
      console.log("User operation failed.");
    }

After running the above snippet, you should see output similar to the following:

Created smart account: 0xA557E90004ba5406A3553897e99D1FC5A2685F6d
Faucet transaction confirmed: 0xa691fcfd1dcacad1ef144461e9c2f1fc110172f0fcfe9a10cbc83e5ca2b6b610
Sending user operation to three destinations...
Waiting for user operation to be confirmed...
User operation confirmed. Block explorer link: https://sepolia.basescan.org/tx/0xd01b2089fd6d4673eae0d7629bcdf5488ff950dba2b7741b4725632f29e9f1ab