With Base Account, you can send multiple onchain calls in a single transaction. Doing so improves the UX of multi-step interactions by reducing them to a single click. A common example of where you might want to leverage batch transactions is an ERC-20 approve
followed by a swap.
You can submit batch transactions by using the wallet_sendCalls
RPC method, defined in EIP-5792.
Install the Base Account SDK:
npm install @base-org/account
Import and create the Base Account SDK instance:
import { createBaseAccountSDK, base } from '@base-org/account';
const sdk = createBaseAccountSDK({
appName: 'Base Account SDK Demo',
appLogoUrl: 'https://base.org/logo.png',
appChainIds: [base.constants.CHAIN_IDS.base],
});
const provider = sdk.getProvider();
Send multiple ETH transfers in a single transaction:
import { createBaseAccountSDK, getCryptoKeyAccount, base } from '@base-org/account';
import { numberToHex, parseEther } from 'viem';
const sdk = createBaseAccountSDK({
appName: 'Batch Transaction Demo',
appLogoUrl: 'https://base.org/logo.png',
appChainIds: [base.constants.CHAIN_IDS.base],
});
const provider = sdk.getProvider();
async function sendBatchTransfers() {
try {
// Get crypto account
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;
// Prepare batch calls
const calls = [
{
to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
value: numberToHex(parseEther('0.001')), // 0.001 ETH
data: '0x', // Empty data for simple transfer
},
{
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f6E456',
value: numberToHex(parseEther('0.001')), // 0.001 ETH
data: '0x', // Empty data for simple transfer
}
];
// Send batch transaction
const result = await provider.request({
method: 'wallet_sendCalls',
params: [{
version: '2.0.0',
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: true, // All calls must succeed or all fail
calls: calls
}]
});
console.log('Batch transaction sent:', result);
return result;
} catch (error) {
console.error('Batch transaction failed:', error);
throw error;
}
}
A common pattern is to approve and then transfer ERC-20 tokens:
import { createBaseAccountSDK, getCryptoKeyAccount, base } from '@base-org/account';
import { numberToHex, parseUnits, encodeFunctionData } from 'viem';
// ERC-20 ABI for approve and transfer functions
const erc20Abi = [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
},
{
name: 'transfer',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
] as const;
async function approveAndTransfer() {
const sdk = createBaseAccountSDK({
appName: 'ERC-20 Batch Demo',
appLogoUrl: 'https://base.org/logo.png',
appChainIds: [base.constants.CHAIN_IDS.base],
});
const provider = sdk.getProvider();
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;
const tokenAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const spenderAddress = '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad'; // Example spender
const recipientAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const amount = parseUnits('10', 6); // 10 USDC (6 decimals)
const calls = [
{
to: tokenAddress,
value: '0x0',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, amount]
})
},
{
to: tokenAddress,
value: '0x0',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, amount]
})
}
];
const result = await provider.request({
method: 'wallet_sendCalls',
params: [{
version: '2.0.0',
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: true,
calls: calls
}]
});
return result;
}
Before sending batch transactions, you can check if the wallet supports atomic batching:
async function checkCapabilities() {
const provider = sdk.getProvider();
try {
const cryptoAccount = await getCryptoKeyAccount();
const address = cryptoAccount?.account?.address;
const capabilities = await provider.request({
method: 'wallet_getCapabilities',
params: [address]
});
const baseCapabilities = capabilities[base.constants.CHAIN_IDS.base];
if (baseCapabilities?.atomicBatch?.supported) {
console.log('Atomic batching is supported');
return true;
} else {
console.log('Atomic batching is not supported');
return false;
}
} catch (error) {
console.error('Failed to check capabilities:', error);
return false;
}
}
Sometimes you want calls to execute sequentially, even if some fail:
const result = await provider.request({
method: 'wallet_sendCalls',
params: [{
version: '2.0.0',
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: false, // Allow partial execution
calls: calls
}]
});
Handle common batch transaction errors:
async function sendBatchWithErrorHandling(calls: any[]) {
try {
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;
const result = await provider.request({
method: 'wallet_sendCalls',
params: [{
version: '2.0.0',
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: true,
calls: calls
}]
});
return { success: true, data: result };
} catch (error: any) {
console.error('Batch transaction error:', error);
if (error.code === 4001) {
return { success: false, error: 'User rejected the transaction' };
} else if (error.code === 5740) {
return { success: false, error: 'Batch too large for wallet to process' };
} else if (error.code === -32602) {
return { success: false, error: 'Invalid request format' };
} else {
return { success: false, error: error.message || 'Unknown error' };
}
}
}
Was this page helpful?