Advanced EVM batch calling library with caching, transformations, and unlimited nesting depth
npm install evm-batchcallbash
npm install evm-batchcall ethers
`
Quick Start
`typescript
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 supply
`
Core Concepts
$3
Execute multiple calls efficiently:
`typescript
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();
`
$3
Transform results on-the-fly:
`typescript
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 BigInt
`
$3
Handle errors gracefully:
`typescript
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.method('balanceOf', [user])
.as('balance')
.fallback('0')
.executeBatch();
`
$3
Enable caching to avoid redundant calls:
`typescript
// 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();
`
$3
Map over array results and create nested calls:
`typescript
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 }
`
$3
Filter items before creating nested calls:
`typescript
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();
`
$3
Add calls conditionally:
`typescript
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();
`
$3
Customize execution behavior:
`typescript
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
});
`
$3
Add multiple method calls in one go:
`typescript
const result = await new BatchCall(provider)
.contract('token', tokenContract)
.methods(
{ method: 'name', args: [] },
{ method: 'symbol', args: [] },
{ method: 'decimals', args: [] }
)
.executeBatch();
`
$3
Reuse configurations:
`typescript
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
$3
`typescript
new BatchCall(provider: ethers.Provider)
`
$3
#### Configuration
- withCache(ttl?: number, shared?: boolean) - Enable caching
- clearCache(clearShared?: boolean) - Clear cache
- setMulticallAddress(address: string) - Set custom Multicall3 address
#### Contract Management
- contract(name: string, instance?: ethers.Contract) - Register/select contract
- registerContracts(registry: ContractRegistryMap) - Register multiple contracts
#### Building Calls
- method(name: string, args?: any[]) - Add a method call
- methods(...calls) - Add multiple method calls
- as(alias: string) - Alias the last call
- transform(fn: (result) => any) - Transform the last call result
- fallback(value: any) - Set fallback for the last call
- when(condition: boolean, callback) - Conditional calls
- mapEach(sourcePath: string, mapper) - Map over array results
- filter(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 structure
- getCallCount() - Get number of calls
- reset() - Clear all calls
- clone() - Clone the instance
#### Static Methods
- BatchCall.clearSharedCache() - Clear all shared cache
$3
`typescript
interface ExecutionOptions {
chunkSize?: number; // Default: 100
retryAttempts?: number; // Default: 3
retryDelay?: number; // Default: 1000ms
continueOnError?: boolean; // Default: false
timeout?: number; // Default: 30000ms
}
`
Advanced Examples
$3
`typescript
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();
`
$3
`typescript
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
1. Use batching: Always prefer executeBatch() over execute()
2. Enable caching: Use withCache() for frequently accessed data
3. Set appropriate chunk sizes: Tune chunkSize based on your RPC limits
4. Use transformations: Process data during batch execution
5. Filter mapEach results: Reduce unnecessary calls with .filter()`