Batch Multiple Transactions

A CRE Connect operation can carry any number of transactions. The Smart Account executes them in order under a single signature; if any sub-call reverts, the entire operation reverts and the on-chain state is rolled back.

When batching matters

PatternWhy batch
approve + transferFromAvoids the two-tx race where a user front-runs your transfer.
wrap + swap + unwrapAll three legs revert together if any fails: no stranded WETH.
Multi-recipient airdropOne signature, one fee, one inclusion guarantee.
Multi-asset rebalanceAtomic invariants across positions.

Build the batch

Each leg is a types.Transaction. Append them in execution order:

import (
    "math/big"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/smartcontractkit/crec-sdk/transact/types"
)

approveData, _ := erc20ABI.Pack("approve", spender, amount)
transferData, _ := vaultABI.Pack("deposit", amount)

op := &types.Operation{
    ID:       big.NewInt(time.Now().Unix()),
    Account:  smartAccount,
    Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
    Transactions: []types.Transaction{
        {
            To:    tokenAddr,
            Value: big.NewInt(0),
            Data:  hexutil.Bytes(approveData),
        },
        {
            To:    vaultAddr,
            Value: big.NewInt(0),
            Data:  hexutil.Bytes(transferData),
        },
    },
}

Sign and submit exactly as you would a single-transaction operation:

opr, err := client.Transact.ExecuteOperation(ctx, channelID, signer, op, chainSelector)

The Smart Account will:

  1. Verify the signature against op.Account.
  2. Check op.Deadline > block.timestamp.
  3. Check op.ID has not been used before.
  4. Loop over op.Transactions and call(to, value, data) for each.
  5. Revert atomically if any sub-call reverts.

Sending native value

Each transaction can carry its own value. The total sum(value) must be available on the Smart Account at execution time:

op.Transactions = []types.Transaction{
    {To: alice, Value: big.NewInt(1e17), Data: nil}, // 0.1 ETH
    {To: bob,   Value: big.NewInt(1e17), Data: nil}, // 0.1 ETH
}

For pure transfers Data can be empty. Top up the Smart Account first if it does not hold the funds; see Wallets: Create and Manage.

Order matters

Transactions execute in the order they appear in Transactions. CRE Connect does not re-order, dedupe, or merge them.

Operation ID rules still apply

op.ID is a per-account nonce. A batched operation uses one ID; the Smart Account does not increment one ID per leg. After confirmation the ID is consumed and cannot be reused.

If you use time.Now().Unix() as the ID and submit two operations within the same second, the second submission will be rejected (already exists from the API or nonce reused from the Smart Account). Bump by one second or use a different scheme:

op.ID = new(big.Int).SetInt64(time.Now().UnixNano()) // higher resolution

Gas considerations

The DON pays gas: there's no per-leg gas limit you need to set. However, every leg in the batch is executed inside a single transaction by the Smart Account, so the combined gas usage of all legs must fit in a single block on the destination chain. If a batch is too large, the operation fails at execution time with the failure reason on the operation.status event. There is no enforced cap from the SDK or API; size your batches against the per-block gas limit of each network you target.

Example: ERC-20 approve-then-deposit

const erc20Json = `[{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]`
const vaultJson = `[{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"}]`

erc20, _ := abi.JSON(strings.NewReader(erc20Json))
vault, _ := abi.JSON(strings.NewReader(vaultJson))

amount := big.NewInt(1_000_000) // 1 USDC
approveCalldata, _ := erc20.Pack("approve", vaultAddr, amount)
depositCalldata, _ := vault.Pack("deposit", amount)

op := &types.Operation{
    ID:       big.NewInt(time.Now().Unix()),
    Account:  smartAccount,
    Deadline: big.NewInt(0),
    Transactions: []types.Transaction{
        {To: usdcAddr, Value: big.NewInt(0), Data: hexutil.Bytes(approveCalldata)},
        {To: vaultAddr, Value: big.NewInt(0), Data: hexutil.Bytes(depositCalldata)},
    },
}

opr, err := client.Transact.ExecuteOperation(ctx, channelID, signer, op, chainSelector)

If the vault deposit reverts (e.g. balance check fails), the approve is rolled back too: the allowance returns to its prior value.

Next steps

Get the latest Chainlink content straight to your inbox.