Multicall groups several contract reads behind one call to an aggregation contract. The original note attached two JavaScript watcher examples and described a 13× speedup, but supplied no workload or timing evidence. That figure is not retained as a benchmark claim.
Configure calls and observe updates
// node scripts_local/multicall/multicall.js
require('dotenv').config();
const { aggregate, createWatcher } = require('@makerdao/multicall');
// https://github.com/makerdao/multicall
let config = {
rpcUrl: 'https://bsc-dataseed1.binance.org:443',
multicallAddress: '0x41263cba59eb80dc200f3e2544eda4ed6a90e76c',
interval: 1000,
};
// Custom config. Default is BSC mainnet
const initConfig = (rpcUrl, multicallAddress, interval = config.interval) => {
config = { rpcUrl, multicallAddress, interval };
};
// Init watcher with your calls
// return watcher
const initWatcher = (cb) => {
const watcher = createWatcher(
[
{
call: ['getEthBalance(address)(uint256)', '0x000000000000000000000000000000000000dead'],
returns: [['BNB_BALANCE', (val) => val / 10 ** 18]],
},
{
target: '0x55d398326f99059ff775485246999027b3197955',
call: ['balanceOf(address)(uint256)', '0x000000000000000000000000000000000000dead'],
returns: [['BALANCE_OF_USDT_DEAD', (val) => val / 10 ** 18]],
},
// https://docs.chain.link/docs/binance-smart-chain-addresses/
{
target: '0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE',
call: ['latestAnswer()(uint256)'],
returns: [['PRICE_FEED_BNB_USD', (val) => val / 10 ** 8]],
},
],
config
);
watcher.subscribe((update) => {
// console.log(`Update: ${update.type} = ${update.value}`);
if (cb) cb(update.type, update.value);
});
return watcher;
};
// Start polling
const start = async (watcher, initialFetchedCb) => {
watcher.start();
await watcher.awaitInitialFetch();
if (initialFetchedCb) initialFetchedCb();
};
// module.exports = {
// initConfig,
// initWatcher,
// start
// };
// Example
const example = async () => {
const watcher = initWatcher((type, value) => {
console.log(`Update: ${type} = ${value}`);
});
await start(watcher, () => {
console.log('Initial fetch completed');
});
// Get initial data manually
watcher
.batch()
.subscribe((updates) => {
console.log('Initial data:', updates);
})
.unsub();
};
example().then();
The first example polls native balance, token balance, and a price-feed value. Its RPC endpoint and contract addresses are historical configuration, not current network recommendations. The aggregation contract must exist and match the selected chain.
Change calls and recreate the watcher
Read the second watcher example
// node scripts_local/multicall/multicall_ex.js
require('dotenv').config();
const { createWatcher } = require('@makerdao/multicall');
const MKR_TOKEN = '0xaaf64bfcc32d0f15873a02163e7e500671a4ffcd';
const MKR_WHALE = '0xdb33dfd3d61308c33c63209845dad3e6bfb2c674';
const MKR_FISH = '0x2dfcedcb401557354d0cf174876ab17bfd6f4efd';
const PRICE_FEED_ETH = '0xa5aA4e07F5255E14F02B385b1f04b35cC50bdb66';
// Preset can be 'mainnet', 'kovan', 'rinkeby', 'goerli' or 'xdai'
// const config = { preset: 'kovan' };
// Alternatively the rpcUrl and multicallAddress can be specified manually
const config = {
rpcUrl: 'wss://kovan.infura.io/ws/v3/' + process.env.INFURA_IO_KEY,
multicallAddress: '0x2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a',
interval: 1000,
};
(async () => {
const watcher = createWatcher(
[
{
call: ['getEthBalance(address)(uint256)', '0x72776bb917751225d24c07d0663b3780b2ada67c'],
returns: [['ETH_BALANCE', (val) => val / 10 ** 18]],
},
{
target: MKR_TOKEN,
call: ['balanceOf(address)(uint256)', MKR_WHALE],
returns: [['BALANCE_OF_MKR_WHALE', (val) => val / 10 ** 18]],
},
{
target: PRICE_FEED_ETH,
call: ['peek()(uint256,bool)'],
returns: [['PRICE_FEED_ETH_PRICE', (val) => val / 10 ** 18], ['PRICE_FEED_ETH_SET']],
},
],
config
);
watcher.subscribe((update) => {
console.log(`Update: ${update.type} = ${update.value}`);
});
watcher.batch().subscribe((updates) => {
// Handle batched updates here
});
watcher.onNewBlock((blockNumber) => {
console.log(`New block: ${blockNumber}`);
});
setTimeout(async () => {
watcher.start();
await watcher.awaitInitialFetch();
console.log('Initial fetch completed');
// Update the calls
setTimeout(() => {
console.log('Updating calls...');
const fetchWaiter = watcher.tap((calls) => [
...calls,
{
target: MKR_TOKEN,
call: ['balanceOf(address)(uint256)', MKR_FISH],
returns: [['BALANCE_OF_MKR_FISH', (val) => val / 10 ** 18]],
},
]);
fetchWaiter.then(() => {
console.log('Initial fetch completed');
});
}, 1000);
// Recreate watcher (useful if network has changed)
setTimeout(() => {
console.log('Recreating with new calls and config...');
const fetchWaiter = watcher.recreate(
[
{
target: MKR_TOKEN,
call: ['balanceOf(address)(uint256)', MKR_WHALE],
returns: [['BALANCE_OF_MKR_WHALE', (val) => val / 10 ** 18]],
},
],
config
);
fetchWaiter.then(() => {
console.log('Initial fetch completed');
});
}, 2000);
}, 1);
// When subscribing to state updates, previously cached values will be returned immediately
// setTimeout(() => {
// console.log(
// 'Subscribing to updates much later (will immediately return cached values)'
// );
// watcher.subscribe(update => {
// console.log(
// `Update (2nd subscription): ${update.type} = ${update.value}`
// );
// });
// watcher.onNewBlock(blockNumber => {
// console.log(`New block (2nd subscription): ${blockNumber}`);
// });
// }, 15000);
})();
(async () => {
await new Promise((res) => {
setTimeout(res, 10000000);
});
})();
This example demonstrates subscription, initial fetch, batched updates, new-block callbacks, and replacing the call set. The complete application must own the poller, timers, subscriptions, and reconnect lifetime. A cached update is not proof that a fresh block has been read.
The source divides token-sized integers into JavaScript numbers. That can lose precision; exact accounting needs an explicit integer/decimal representation. An aggregate read can align subcalls within its execution, while separate polling rounds still need their block identity recorded.
Source: Gist revision. No RPC requests or speed measurements were performed for this revision.