@ethereum-js/multicall
Version:
Multicall allows multiple smart contract constant function calls to be grouped into a single call and the results aggregated into a single result
531 lines (515 loc) • 14.4 kB
text/typescript
import { ethers, Provider } from "ethers";
import { CallOptions, CallData, MulticalSetup, ExecuteData } from "./types";
import { ethers as etherV5 } from "ethers-v5";
export enum Networks {
mainnet = 1,
ropsten = 3,
rinkeby = 4,
goerli = 5,
optimism = 10,
kovan = 42,
matic = 137,
kovanOptimism = 69,
xdai = 100,
goerliOptimism = 420,
arbitrum = 42161,
rinkebyArbitrum = 421611,
goerliArbitrum = 421613,
mumbai = 80001,
sepolia = 11155111,
avalancheMainnet = 43114,
avalancheFuji = 43113,
fantomTestnet = 4002,
fantom = 250,
bsc = 56,
bsc_testnet = 97,
moonbeam = 1284,
moonriver = 1285,
moonbaseAlphaTestnet = 1287,
harmony = 1666600000,
cronos = 25,
fuse = 122,
songbirdCanaryNetwork = 19,
costonTestnet = 16,
boba = 288,
aurora = 1313161554,
astar = 592,
okc = 66,
heco = 128,
metis = 1088,
rsk = 30,
rskTestnet = 31,
evmos = 9001,
evmosTestnet = 9000,
thundercore = 108,
thundercoreTestnet = 18,
oasis = 26863,
celo = 42220,
godwoken = 71402,
godwokentestnet = 71401,
klatyn = 8217,
milkomeda = 2001,
kcc = 321,
etherlite = 111,
lineaTestnet = 59140,
}
export const getContractAddressFromChainId = (chainId: number) => {
switch (chainId) {
case Networks.mainnet:
case Networks.ropsten:
case Networks.rinkeby:
case Networks.goerli:
case Networks.optimism:
case Networks.kovan:
case Networks.matic:
case Networks.kovanOptimism:
case Networks.xdai:
case Networks.goerliOptimism:
case Networks.arbitrum:
case Networks.rinkebyArbitrum:
case Networks.goerliArbitrum:
case Networks.mumbai:
case Networks.sepolia:
case Networks.avalancheMainnet:
case Networks.avalancheFuji:
case Networks.fantomTestnet:
case Networks.fantom:
case Networks.bsc:
case Networks.bsc_testnet:
case Networks.moonbeam:
case Networks.moonriver:
case Networks.moonbaseAlphaTestnet:
case Networks.harmony:
case Networks.cronos:
case Networks.fuse:
case Networks.songbirdCanaryNetwork:
case Networks.costonTestnet:
case Networks.boba:
case Networks.aurora:
case Networks.astar:
case Networks.okc:
case Networks.heco:
case Networks.metis:
case Networks.rsk:
case Networks.rskTestnet:
case Networks.evmos:
case Networks.evmosTestnet:
case Networks.thundercore:
case Networks.thundercoreTestnet:
case Networks.oasis:
case Networks.celo:
case Networks.godwoken:
case Networks.godwokentestnet:
case Networks.klatyn:
case Networks.milkomeda:
case Networks.kcc:
case Networks.lineaTestnet:
return "0xcA11bde05977b3631167028862bE2a173976CA11";
case Networks.etherlite:
return "0x21681750D7ddCB8d1240eD47338dC984f94AF2aC";
default:
throw new Error(
`Chain ${chainId} doesn't have a multicall contract address`
);
}
};
export default class Ethers {
static isV5 = !ethers.ZeroAddress;
static v5 = etherV5;
static v6 = ethers;
static getInterface(abi: any[]) {
let iface: etherV5.utils.Interface | ethers.Interface;
if (this.isV5) {
iface = new etherV5.utils.Interface(JSON.stringify(abi));
} else {
iface = ethers.Interface.from(abi);
}
return iface;
}
static getOutput(abi: any[], method: string) {
const iface = this.getInterface(abi);
if (iface.getFunction(method)?.outputs) {
return iface.getFunction(method)?.outputs;
}
for (let i = 0; i < abi.length; i++) {
if (abi[i].name?.trim() === method) {
return abi[i].outputs;
}
}
return null;
}
static abiDecode(types: any, data: any) {
if (this.isV5) {
return etherV5.utils.defaultAbiCoder.decode(types, data);
} else {
return ethers.AbiCoder.defaultAbiCoder().decode(types, data);
}
}
}
const abi = [
{
inputs: [
{
components: [
{ internalType: "address", name: "target", type: "address" },
{ internalType: "bytes", name: "callData", type: "bytes" },
],
internalType: "struct Multicall3.Call[]",
name: "calls",
type: "tuple[]",
},
],
name: "aggregate",
outputs: [
{ internalType: "uint256", name: "blockNumber", type: "uint256" },
{ internalType: "bytes[]", name: "returnData", type: "bytes[]" },
],
stateMutability: "payable",
type: "function",
},
{
inputs: [
{
components: [
{ internalType: "address", name: "target", type: "address" },
{ internalType: "bool", name: "allowFailure", type: "bool" },
{ internalType: "bytes", name: "callData", type: "bytes" },
],
internalType: "struct Multicall3.Call3[]",
name: "calls",
type: "tuple[]",
},
],
name: "aggregate3",
outputs: [
{
components: [
{ internalType: "bool", name: "success", type: "bool" },
{ internalType: "bytes", name: "returnData", type: "bytes" },
],
internalType: "struct Multicall3.Result[]",
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "payable",
type: "function",
},
{
inputs: [
{
components: [
{ internalType: "address", name: "target", type: "address" },
{ internalType: "bool", name: "allowFailure", type: "bool" },
{ internalType: "uint256", name: "value", type: "uint256" },
{ internalType: "bytes", name: "callData", type: "bytes" },
],
internalType: "struct Multicall3.Call3Value[]",
name: "calls",
type: "tuple[]",
},
],
name: "aggregate3Value",
outputs: [
{
components: [
{ internalType: "bool", name: "success", type: "bool" },
{ internalType: "bytes", name: "returnData", type: "bytes" },
],
internalType: "struct Multicall3.Result[]",
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "payable",
type: "function",
},
{
inputs: [
{
components: [
{ internalType: "address", name: "target", type: "address" },
{ internalType: "bytes", name: "callData", type: "bytes" },
],
internalType: "struct Multicall3.Call[]",
name: "calls",
type: "tuple[]",
},
],
name: "blockAndAggregate",
outputs: [
{ internalType: "uint256", name: "blockNumber", type: "uint256" },
{ internalType: "bytes32", name: "blockHash", type: "bytes32" },
{
components: [
{ internalType: "bool", name: "success", type: "bool" },
{ internalType: "bytes", name: "returnData", type: "bytes" },
],
internalType: "struct Multicall3.Result[]",
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "payable",
type: "function",
},
{
inputs: [],
name: "getBasefee",
outputs: [{ internalType: "uint256", name: "basefee", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "uint256", name: "blockNumber", type: "uint256" }],
name: "getBlockHash",
outputs: [{ internalType: "bytes32", name: "blockHash", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getBlockNumber",
outputs: [
{ internalType: "uint256", name: "blockNumber", type: "uint256" },
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getChainId",
outputs: [{ internalType: "uint256", name: "chainid", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockCoinbase",
outputs: [{ internalType: "address", name: "coinbase", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockDifficulty",
outputs: [{ internalType: "uint256", name: "difficulty", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockGasLimit",
outputs: [{ internalType: "uint256", name: "gaslimit", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockTimestamp",
outputs: [{ internalType: "uint256", name: "timestamp", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "address", name: "addr", type: "address" }],
name: "getEthBalance",
outputs: [{ internalType: "uint256", name: "balance", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getLastBlockHash",
outputs: [{ internalType: "bytes32", name: "blockHash", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ internalType: "bool", name: "requireSuccess", type: "bool" },
{
components: [
{ internalType: "address", name: "target", type: "address" },
{ internalType: "bytes", name: "callData", type: "bytes" },
],
internalType: "struct Multicall3.Call[]",
name: "calls",
type: "tuple[]",
},
],
name: "tryAggregate",
outputs: [
{
components: [
{ internalType: "bool", name: "success", type: "bool" },
{ internalType: "bytes", name: "returnData", type: "bytes" },
],
internalType: "struct Multicall3.Result[]",
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "payable",
type: "function",
},
{
inputs: [
{ internalType: "bool", name: "requireSuccess", type: "bool" },
{
components: [
{ internalType: "address", name: "target", type: "address" },
{ internalType: "bytes", name: "callData", type: "bytes" },
],
internalType: "struct Multicall3.Call[]",
name: "calls",
type: "tuple[]",
},
],
name: "tryBlockAndAggregate",
outputs: [
{ internalType: "uint256", name: "blockNumber", type: "uint256" },
{ internalType: "bytes32", name: "blockHash", type: "bytes32" },
{
components: [
{ internalType: "bool", name: "success", type: "bool" },
{ internalType: "bytes", name: "returnData", type: "bytes" },
],
internalType: "struct Multicall3.Result[]",
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "payable",
type: "function",
},
];
export class Multicall {
private _setup: MulticalSetup;
constructor(setup: MulticalSetup) {
if (!setup.provider) {
throw Error("Provider is required");
}
this._setup = setup;
}
public async call(
contractsCallData: CallData[],
callOptions: CallOptions = {}
) {
const executeData: ExecuteData[] = [];
for (
let callDataIndex = 0;
callDataIndex < contractsCallData.length;
callDataIndex++
) {
const callData = contractsCallData[callDataIndex];
const iface = Ethers.getInterface(callData.abi);
executeData.push({
callData: iface.encodeFunctionData(
callData.method,
callData.parameters
),
target: callData.contractAddress,
outputTypes: Ethers.getOutput(callData.abi, callData.method),
});
}
return this.execute(executeData, callOptions);
}
private async execute(executeData: ExecuteData[], callOptions: CallOptions) {
let response;
if (Ethers.isV5) {
response = await this.executeEtherV5(executeData, callOptions);
} else {
response = await this.executeEtherV6(executeData, callOptions);
}
const results: any[] = [];
response.returnData.forEach(
(result: { success: boolean; returnData: any }, index: number) => {
if (result.success) {
if (executeData[index].outputTypes) {
const decoded = Ethers.abiDecode(
executeData[index].outputTypes,
result.returnData
);
const data = decoded.length === 1 ? decoded[0] : decoded;
results.push(data);
} else {
results.push(result.returnData);
}
} else {
results.push(null);
}
}
);
return {
blockNumber: parseInt(`${response.blockNumber}`),
results,
};
}
private async executeEtherV5(
executeData: ExecuteData[],
callOptions: CallOptions
) {
const provider = this._setup.provider;
let contractMulticallAddress = this._setup.contractMulticall;
if (!contractMulticallAddress) {
const network = await provider.getNetwork();
contractMulticallAddress = getContractAddressFromChainId(
parseInt(`${network.chainId}`)
);
}
const contract = new Ethers.v5.Contract(
contractMulticallAddress,
abi,
provider as any
);
let options = {};
if (callOptions.blockNumber) {
options = {
...options,
blockTag: parseInt(`${callOptions.blockNumber}`),
};
}
const response = await contract.callStatic.tryBlockAndAggregate(
false,
executeData.map((ele) => ({
target: ele.target,
callData: ele.callData,
})),
options
);
return response;
}
private async executeEtherV6(
executeData: ExecuteData[],
callOptions: CallOptions
) {
const provider = this._setup.provider as Provider;
let contractMulticallAddress = this._setup.contractMulticall;
if (!contractMulticallAddress) {
const network = await provider.getNetwork();
contractMulticallAddress = getContractAddressFromChainId(
parseInt(`${network.chainId}`)
);
}
const contract = new Ethers.v6.Contract(contractMulticallAddress, abi, {
provider,
});
let options = {};
if (callOptions.blockNumber) {
options = {
...options,
blockTag: parseInt(`${callOptions.blockNumber}`),
};
}
const response = await contract.tryBlockAndAggregate.staticCall(
false,
executeData.map((ele) => ({
target: ele.target,
callData: ele.callData,
})),
options
);
return response;
}
}