evm-batchcall
v1.0.1
Published
Advanced EVM batch calling library with caching, transformations, and unlimited nesting depth
Maintainers
Readme
EVM BatchCall
Advanced EVM batch calling library with caching, transformations, and unlimited nesting depth. Built on top of Multicall3 for efficient blockchain data retrieval.
Features
- 🚀 Batch Execution: Execute multiple contract calls in a single request
- 💾 Smart Caching: Built-in caching with TTL support (instance or shared)
- 🔄 Transformations: Transform results on-the-fly
- 🎯 MapEach: Map over arrays and create nested calls with unlimited depth
- 🛡️ Error Handling: Fallback values and retry mechanisms
- ⚡ Performance: Automatic chunking for large batches
- 🎨 Type Safe: Full TypeScript support
Installation
npm install evm-batchcall ethersQuick Start
import { BatchCall } from 'evm-batchcall';
import { ethers } from 'ethers';
// Initialize provider
const provider = new ethers.JsonRpcProvider('YOUR_RPC_URL');
// Create contract instances
const tokenContract = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, provider);
// Execute batch calls
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.method('balanceOf', [userAddress])
.as('balance')
.method('totalSupply', [])
.as('totalSupply')
.executeBatch();
console.log(result.balance); // User's balance
console.log(result.totalSupply); // Token total supplyCore Concepts
1. Basic Batching
Execute multiple calls efficiently:
const result = await new BatchCall(provider)
.registerContracts({
token: tokenContract,
nft: nftContract
})
.contract('token')
.method('balanceOf', [user])
.as('tokenBalance')
.contract('nft')
.method('balanceOf', [user])
.as('nftBalance')
.executeBatch();2. Transformations
Transform results on-the-fly:
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.method('balanceOf', [user])
.as('balance')
.transform(balance => ethers.formatEther(balance))
.executeBatch();
console.log(result.balance); // "1.5" instead of BigInt3. Fallback Values
Handle errors gracefully:
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.method('balanceOf', [user])
.as('balance')
.fallback('0')
.executeBatch();4. Caching
Enable caching to avoid redundant calls:
// Shared cache (default) - cached across all instances
const result1 = await new BatchCall(provider)
.withCache(30000) // Cache for 30 seconds
.contract('token', tokenContract)
.method('totalSupply', [])
.executeBatch();
// This will hit the cache!
const result2 = await new BatchCall(provider)
.withCache(30000)
.contract('token', tokenContract)
.method('totalSupply', [])
.executeBatch();
// Clear shared cache
BatchCall.clearSharedCache();
// Or use instance-only cache
const result3 = await new BatchCall(provider)
.withCache(30000, false) // shared = false
.contract('token', tokenContract)
.method('totalSupply', [])
.executeBatch();5. MapEach - Nested Calls
Map over array results and create nested calls:
const result = await new BatchCall(provider)
.contract('launchpad', launchpadContract)
.contract('token', tokenContract)
.contract('launchpad')
.method('getAllLaunchpads', [])
.as('launchpadIds')
.mapEach('launchpadIds', (id) => [
{
contract: 'launchpad',
method: 'launchpads',
args: [id],
alias: 'data',
transform: (data) => ({ ...data, id })
},
{
contract: 'token',
method: 'balanceOf',
args: [id],
alias: 'balance',
fallback: 0n
}
])
.as('details')
.executeBatch();
console.log(result.details); // Array of { data, balance }6. Filtering MapEach Results
Filter items before creating nested calls:
const result = await new BatchCall(provider)
.contract('nft', nftContract)
.method('getAllTokenIds', [])
.as('tokenIds')
.mapEach('tokenIds', (tokenId) => [
{ contract: 'nft', method: 'ownerOf', args: [tokenId], alias: 'owner' }
])
.filter((tokenId) => tokenId < 1000) // Only process first 1000
.as('owners')
.executeBatch();7. Conditional Calls
Add calls conditionally:
const includeAllowance = true;
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.method('balanceOf', [user])
.as('balance')
.when(includeAllowance, (bc) => {
bc.method('allowance', [user, spender]).as('allowance');
})
.executeBatch();8. Advanced Options
Customize execution behavior:
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.method('balanceOf', [user])
.executeBatch({
chunkSize: 50, // Split into chunks of 50 calls
retryAttempts: 5, // Retry failed calls 5 times
retryDelay: 2000, // Wait 2s between retries
continueOnError: true, // Don't stop on errors
timeout: 60000 // 60s timeout per call
});9. Multiple Methods at Once
Add multiple method calls in one go:
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.methods(
{ method: 'name', args: [] },
{ method: 'symbol', args: [] },
{ method: 'decimals', args: [] }
)
.executeBatch();10. Clone and Reuse
Reuse configurations:
const baseCall = new BatchCall(provider)
.contract('token', tokenContract);
const balance1 = await baseCall.clone()
.method('balanceOf', [user1])
.executeBatch();
const balance2 = await baseCall.clone()
.method('balanceOf', [user2])
.executeBatch();API Reference
Constructor
new BatchCall(provider: ethers.Provider)Methods
Configuration
withCache(ttl?: number, shared?: boolean)- Enable cachingclearCache(clearShared?: boolean)- Clear cachesetMulticallAddress(address: string)- Set custom Multicall3 address
Contract Management
contract(name: string, instance?: ethers.Contract)- Register/select contractregisterContracts(registry: ContractRegistryMap)- Register multiple contracts
Building Calls
method(name: string, args?: any[])- Add a method callmethods(...calls)- Add multiple method callsas(alias: string)- Alias the last calltransform(fn: (result) => any)- Transform the last call resultfallback(value: any)- Set fallback for the last callwhen(condition: boolean, callback)- Conditional callsmapEach(sourcePath: string, mapper)- Map over array resultsfilter(predicate)- Filter mapEach items (chain after mapEach)
Execution
execute(options?)- Execute sequentially (no batching)executeBatch(options?)- Execute in batch using Multicall3
Utilities
buildCalls()- Get raw multicall structuregetCallCount()- Get number of callsreset()- Clear all callsclone()- Clone the instance
Static Methods
BatchCall.clearSharedCache()- Clear all shared cache
Execution Options
interface ExecutionOptions {
chunkSize?: number; // Default: 100
retryAttempts?: number; // Default: 3
retryDelay?: number; // Default: 1000ms
continueOnError?: boolean; // Default: false
timeout?: number; // Default: 30000ms
}Advanced Examples
Complex DeFi Query
const defiData = await new BatchCall(provider)
.withCache(60000) // Cache for 1 minute
.registerContracts({
pool: poolContract,
token0: token0Contract,
token1: token1Contract,
router: routerContract
})
.contract('pool')
.method('getReserves', [])
.as('reserves')
.transform(([r0, r1]) => ({ reserve0: r0, reserve1: r1 }))
.method('totalSupply', [])
.as('lpTotalSupply')
.contract('token0')
.method('balanceOf', [userAddress])
.as('token0Balance')
.method('decimals', [])
.as('token0Decimals')
.contract('token1')
.method('balanceOf', [userAddress])
.as('token1Balance')
.method('decimals', [])
.as('token1Decimals')
.executeBatch();NFT Collection Analysis
const nftData = await new BatchCall(provider)
.contract('nft', nftContract)
.method('totalSupply', [])
.as('totalSupply')
.transform(supply => Number(supply))
.mapEach('totalSupply', (_, index) => {
const tokenId = index + 1;
return [
{
contract: 'nft',
method: 'ownerOf',
args: [tokenId],
alias: 'owner',
fallback: ethers.ZeroAddress
},
{
contract: 'nft',
method: 'tokenURI',
args: [tokenId],
alias: 'uri',
fallback: ''
}
];
})
.filter((_, index) => index < 100) // First 100 only
.as('tokens')
.executeBatch({ chunkSize: 25 });Network Support
Works on any EVM-compatible network that has Multicall3 deployed:
- Ethereum Mainnet
- Polygon
- Arbitrum
- Optimism
- Base
- BSC
- Avalanche
- And more...
The default Multicall3 address (0xcA11bde05977b3631167028862bE2a173976CA11) is deployed on most networks. For custom deployments, use setMulticallAddress().
Performance Tips
- Use batching: Always prefer
executeBatch()overexecute() - Enable caching: Use
withCache()for frequently accessed data - Set appropriate chunk sizes: Tune
chunkSizebased on your RPC limits - Use transformations: Process data during batch execution
- Filter mapEach results: Reduce unnecessary calls with
.filter()
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues and questions, please open an issue on GitHub.
