Wallets

Batch Operations

Submit multiple user operations as a batch

Both Developer-Controlled Wallets and Modular Smart Contract Accounts (MSCAs) support batching user operations into a single atomic transaction. These batched operations have several advantages:

  • Users only need to wait for a single batched transaction to execute, instead of multiple individual transactions.
  • Users pay less in gas fees, as batched transactions cost less than multiple individual transactions.
  • Transactions are atomic: if any transaction in the batch reverts, the entire batch reverts.

You can batch transactions through the contractExecution endpoint.

The following example demonstrates how to generate a call to the contractExecution endpoint that batches two USDC transfer transactions. This example uses an MSCA wallet, on the ETH-SEPOLIA testnet.

This code creates the encoding of the transfer function calls that make up the abiParameters data for the call to the contractExecution endpoint:

JavaScript
const { ethers } = require('ethers')

const abiEncode = async function () {
  // encode callData for transfers
  const abiTransfer = ['function transfer(address recipient, uint256 amount)']
  const contractInterfaceTransfer = new ethers.utils.Interface(abiTransfer)
  let data = contractInterfaceTransfer.encodeFunctionData('transfer', [
    '0xa1404d9E7646b0112C49aE0296D6347C956D0867',
    100000000, // 100 usdc
  ])
  console.log(data)
}

abiEncode()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

Note that the preceding code sample encodes only a single call to the transfer function, the call is then specified twice in the abiParameters field of the call to the contractExecution endpoint.

JSON
{
  "idempotencyKey": "{{$randomUUID}}",
  "walletId": "{{wallet-id}}",
  "feeLevel": "HIGH",
  "refId": "test batch transfer",

  "contractAddress": "0xcd3d4d703f1884313e8f3188a8c60cc6d389ebd1", // sca wallet address

  "abiFunctionSignature": "executeBatch((address, uint256, bytes)[])",
  "abiParameters": [
    [
      [
        "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
        "0",
        "0xa9059cbb000000000000000000000000a1404d9e7646b0112c49ae0296d6347c956d08670000000000000000000000000000000000000000000000000000000000000001"
      ],
      [
        "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
        "0",
        "0xa9059cbb000000000000000000000000a1404d9e7646b0112c49ae0296d6347c956d08670000000000000000000000000000000000000000000000000000000000000001"
      ]
    ]
  ],

  "entitySecretCiphertext": "{{entity-secret-ciphertext}}"
}

The function signature called is executeBatch((address address, uint256 amount, bytes func)[]). The address is the USDC contract address, amount is 0 (the amount of native token transferred), and func is the encoded function call generated by the sample code. The example provides the parameters twice, so the resulting batched transactions result in 200 USDC transferred (100 for each transaction).

The following example demonstrates how to generate a call to the contractExecution endpoint that batches two related transactions: approve, and depositForBurn in the CCTP contract. This example uses an MSCA wallet, on the ETH-SEPOLIA testnet.

This code creates the encoding of the two calls that make up the abiParameters data for the call to the contractExecution endpoint.

JavaScript
const { ethers } = require('ethers')

const abiEncode = async function () {
  const abiDepositForBurn = [
    'function depositForBurn(uint256,uint32,bytes32,address)',
  ]
  const abiApprove = ['function approve(address,uint256)']
  const contractInterfaceApprove = new ethers.utils.Interface(abiApprove)
  const contractInterfaceDepositForBurn = new ethers.utils.Interface(
    abiDepositForBurn,
  )

  // encode approve
  let dataApprove = contractInterfaceApprove.encodeFunctionData('approve', [
    '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5',
    100000000, // 100 usdc
  ])
  console.log(dataApprove)

  // encode sca wallet address
  const abi = ethers.utils.defaultAbiCoder
  let encodedAddress = abi.encode(
    ['address'],
    ['0xc4b69f1f26c820e7fa18c925da8bf2b2a3357fe0'],
  )
  console.log(encodedAddress)

  // encode depositForBurn
  let dataDepositForBurn = contractInterfaceDepositForBurn.encodeFunctionData(
    'depositForBurn',
    [
      100000000,
      2, // AVAX
      encodedAddress, // encoded sca wallet address
      '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', // usdc contract
    ],
  )
  console.log(dataDepositForBurn)
}

abiEncode()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

The first transaction in the batch is a call to the function on the USDC contract: approve(address spender, uint256 value). Calling approve allows the spender to spend value USDC on your behalf. This is necessary for the subsequent depositForBurn transaction in the batch. In this example, address is the TokenMessenger contract address and the value is 100000000 (100 USDC). Note that the example code encodes the parameters of the function call, the subsequent call to the Circle API specifies the contract address to make the call to.

The second transaction in the batch is a call to the function on the TokenMessenger contract: depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken). Calling depositForBurn results in the TokenMessenger contract transferring and burning the specified amount of USDC from the wallet on the origin chain. The parameters to the depositForBurn call are:

  • amount: the amount of USDC to burn. In this example, 100000000 (100 USDC)
  • destinationDomain: a Circle-issued identifier for a network where Circle deploys CCTP contracts. In this example, 2 to specify Avalanche. For a full list, see the CCTP domain list.
  • mintRecipient: the destination address, encoded as a Solidity address.
  • burnToken: the contract address of the token to burn. In this example, the USDC contract on the source chain.

The preceding code results in a request to the contractExecution endpoint like this:

JSON
{
  "idempotencyKey": "{{$randomUUID}}",
  "walletId": "{{wallet-id}}",
  "feeLevel": "HIGH",
  "refId": "test batch CCTP",
  "contractAddress": "0xcd3d4d703f1884313e8f3188a8c60cc6d389ebd1", // sca wallet address
  "abiFunctionSignature": "executeBatch((address, uint256, bytes)[])",
  "abiParameters": [
    [
      [
        "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
        "0",
        "0x095ea7b30000000000000000000000009f3b8679c73c2fef8b59b4f3444d4e156fb70aa500000000000000000000000000000000000000000000000000000000000003e8"
      ],
      [
        "0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5",
        "0",
        "0x6fd3504e00000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c4b69f1f26c820e7fa18c925da8bf2b2a3357fe00000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238"
      ]
    ]
  ],
  "entitySecretCiphertext": "{{entity-secret-ciphertext}}"
}

The function signature called is executeBatch((address address, uint256 amount, bytes func)[]). The parameters for each transaction are:

  • approve

    • address is the USDC token contract address
    • amount is the amount of native tokens transferred
    • func is the encoded function call generated by the sample code
  • depositForBurn

    • address is the TokenMessenger contract address
    • amount is the amount of native tokens transferred
    • func is the encoded function call generated by the sample code
Did this page help you?
© 2023-2025 Circle Technology Services, LLC. All rights reserved.