UNPKG

@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
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; } }