Skip to main content

Batch Transactions

Execute multiple onchain actions in a single request with Volr's batch transaction feature.

What are Batch Transactions?

Batch transactions allow you to bundle multiple onchain actions into a single request. Users approve once, and Volr executes all transactions atomically — either all succeed or all fail together.

Benefits

  • Atomic execution: All transactions succeed or fail together
  • Better UX: Users approve once instead of multiple times
  • Gas efficient: Reduced gas costs compared to separate transactions
  • Perfect for complex operations: Ideal for swaps, bridges, and DeFi operations

Use Cases

Token Swaps

Instead of requiring users to:

  1. Approve token spending
  2. Execute swap

You can bundle both actions:

import { useVolr } from '@volr/react';

function SwapButton() {
const { evm } = useVolr();

const handleSwap = async () => {
const client = evm.client(8453); // Base chain

await client.sendBatch([
{
target: tokenAddress,
data: approveCalldata,
value: 0n,
gasLimit: 100000n,
},
{
target: swapRouterAddress,
data: swapCalldata,
value: 0n,
gasLimit: 200000n,
},
]);
};

return <button onClick={handleSwap}>Swap Tokens</button>;
}

Cross-Chain Bridges

Bundle multiple operations for bridging:

const { evm } = useVolr();
const client = evm.client(chainId);

await client.sendBatch([
{
target: sourceTokenAddress,
data: approveCalldata,
value: 0n,
gasLimit: 100000n,
},
{
target: bridgeContractAddress,
data: bridgeCalldata,
value: 0n,
gasLimit: 300000n,
},
]);

Complex DeFi Operations

Execute multiple DeFi actions atomically:

const { evm } = useVolr();
const client = evm.client(chainId);

await client.sendBatch([
{
target: lendingProtocol,
data: depositCalldata,
value: 0n,
gasLimit: 150000n,
},
{
target: stakingContract,
data: stakeCalldata,
value: 0n,
gasLimit: 150000n,
},
]);

How It Works

[User initiates batch]

[User approves once]

[Volr bundles transactions]

[All transactions executed atomically]

[All succeed or all fail]

Gas Sponsorship with Batch Transactions

Batch transactions work seamlessly with gas sponsorship:

  • Developer sponsors gas: You pay gas fees for all transactions in the batch
  • User pays only for actions: Users don't need native tokens
  • Single approval: Users approve once for the entire batch

Best Practices

Only batch transactions that should succeed or fail together:

// Good: Related actions
const swapBatch = [approveTx, swapTx];

// Bad: Unrelated actions
const unrelatedBatch = [swapTx, transferTx, stakeTx];

2. Consider Gas Limits

Each transaction in the batch consumes gas. Ensure your gas sponsorship limits accommodate the full batch:

// Check total gas estimate before batching
const totalGas = transactions.reduce((sum, tx) => sum + tx.gasLimit, 0n);
if (totalGas > maxGasLimit) {
// Split into smaller batches or increase limit
}

3. Handle Errors Gracefully

Since batch transactions are atomic, handle failures appropriately:

try {
await client.sendBatch(transactions);
} catch (error) {
if (error.message.includes('user rejected')) {
// User cancelled
} else {
// Transaction failed - all transactions in batch are reverted
console.error('Batch execution failed:', error);
}
}

API Reference

sendBatch Method

Use useVolr().evm.client(chainId).sendBatch() to execute batch transactions:

import { useVolr } from '@volr/react';

const { evm } = useVolr();
const client = evm.client(chainId);

await client.sendBatch(calls, options);

Method 1: Pre-built Call Objects

await client.sendBatch([
{
target: '0x...', // Contract address
data: '0x...', // Transaction calldata
value: 0n, // ETH value (optional, default 0n)
gasLimit: 100000n, // Gas limit (optional)
},
]);

Method 2: BuildCallOptions (Type-safe)

await client.sendBatch([
{
target: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, amount],
gasLimit: 100000n,
},
{
target: routerAddress,
abi: routerAbi,
functionName: 'swap',
args: [tokenIn, tokenOut, amount],
gasLimit: 200000n,
},
]);

Parameters

  • calls: Array of transaction objects
    • target: Contract address (0x${string})
    • data: Transaction calldata (for pre-built calls)
    • abi, functionName, args: For type-safe calls
    • value: Optional ETH value (bigint, default 0n)
    • gasLimit: Optional gas limit (bigint)
  • options: Optional settings
    • from: Override sender address
    • expiresInSec: Transaction expiry time

Returns

interface RelayResult {
txHash: string;
userOpHash?: string;
status: 'confirmed' | 'pending';
}

Examples

Complete Swap Example

import { useVolr } from '@volr/react';
import { encodeFunctionData, parseUnits } from 'viem';

const erc20Abi = [
{
name: 'approve',
type: 'function',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ type: 'bool' }],
},
] as const;

function TokenSwap() {
const { evm } = useVolr();

const handleSwap = async () => {
const chainId = 8453; // Base
const tokenAddress = '0x...'; // Token to swap
const routerAddress = '0x...'; // DEX router
const amount = parseUnits('100', 18);

const client = evm.client(chainId);

// Option 1: Using type-safe BuildCallOptions
try {
const result = await client.sendBatch([
{
target: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [routerAddress, amount],
gasLimit: 100000n,
},
{
target: routerAddress,
abi: swapRouterAbi,
functionName: 'swapExactTokensForTokens',
args: [amount, 0n, [tokenAddress, usdcAddress], userAddress, BigInt(Date.now() + 1000 * 60 * 20)],
gasLimit: 200000n,
},
]);

console.log('Swap successful:', result.txHash);
} catch (error) {
console.error('Swap failed:', error);
}
};

return <button onClick={handleSwap}>Swap Tokens</button>;
}

Using Pre-encoded Calldata

import { useVolr } from '@volr/react';
import { encodeFunctionData } from 'viem';

function SwapWithCalldata() {
const { evm } = useVolr();

const handleSwap = async () => {
const client = evm.client(8453);

// Encode calldata manually
const approveData = encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [routerAddress, amount],
});

const swapData = encodeFunctionData({
abi: swapRouterAbi,
functionName: 'swap',
args: [tokenIn, tokenOut, amount],
});

const result = await client.sendBatch([
{ target: tokenAddress, data: approveData, value: 0n, gasLimit: 100000n },
{ target: routerAddress, data: swapData, value: 0n, gasLimit: 200000n },
]);

console.log('TX Hash:', result.txHash);
};

return <button onClick={handleSwap}>Swap</button>;
}

Limitations

  • Gas limits: Each transaction in the batch must respect gas limits
  • Atomic execution: All transactions succeed or fail together — no partial success
  • Complexity: More complex batches may require higher gas limits

Next Steps