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:
- Approve token spending
- 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
1. Group Related Actions
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 objectstarget: Contract address (0x${string})data: Transaction calldata (for pre-built calls)abi,functionName,args: For type-safe callsvalue: Optional ETH value (bigint, default 0n)gasLimit: Optional gas limit (bigint)
options: Optional settingsfrom: Override sender addressexpiresInSec: 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
- Transactions - Understand basic transaction handling
- Gas Sponsorship - Learn about gas fee sponsorship
- Account Management - Manage user accounts