UNPKG

@morpho-labs/ethers-multicall

Version:

⚡🚀 Call multiple view functions, from multiple Smart Contracts, in a single RPC query!

142 lines (141 loc) 8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EthersMulticall = exports.isMulticallUnderlyingError = void 0; const dataloader_1 = __importDefault(require("dataloader")); const utils_1 = require("ethers/lib/utils"); const contracts_1 = require("./contracts"); const isMulticallUnderlyingError = (err) => err.message.includes("Multicall call failed for"); exports.isMulticallUnderlyingError = isMulticallUnderlyingError; const DIGIT_REGEX = /^\d+$/; const DEFAULT_DATALOADER_OPTIONS = { cache: true, maxBatchSize: 512 }; class EthersMulticall { constructor(provider, { defaultBlockTag = "latest", options = DEFAULT_DATALOADER_OPTIONS, } = {}) { this.multicall = contracts_1.Multicall3__factory.connect( // same address on all networks (cf. https://github.com/mds1/multicall#deployments) "0xcA11bde05977b3631167028862bE2a173976CA11", provider); this.dataLoader = new dataloader_1.default( // @ts-ignore this.doCalls.bind(this), options); this.defaultBlockTag = defaultBlockTag; } get contract() { return this.multicall; } setProvider(provider, chainId) { return __awaiter(this, void 0, void 0, function* () { chainId !== null && chainId !== void 0 ? chainId : (chainId = (yield provider.getNetwork()).chainId); this.multicall = contracts_1.Multicall3__factory.connect(this.multicall.address, provider); }); } doCalls(allCalls) { return __awaiter(this, void 0, void 0, function* () { const resolvedCalls = yield Promise.all(allCalls.map((call, index) => __awaiter(this, void 0, void 0, function* () { return (Object.assign(Object.assign({}, call), { index, overrides: call.overrides ? yield (0, utils_1.resolveProperties)(call.overrides) : undefined })); }))); const blockTagCalls = resolvedCalls.reduce((acc, call) => { var _a, _b, _c; const blockTag = ((_b = (_a = call.overrides) === null || _a === void 0 ? void 0 : _a.blockTag) !== null && _b !== void 0 ? _b : this.defaultBlockTag).toString(); return Object.assign(Object.assign({}, acc), { [blockTag]: [call].concat((_c = acc[blockTag]) !== null && _c !== void 0 ? _c : []) }); }, {}); const results = []; yield Promise.all(Object.entries(blockTagCalls).map(([blockTagStr, calls]) => __awaiter(this, void 0, void 0, function* () { const callStructs = calls.map((call) => ({ target: call.address, callData: new utils_1.Interface([]).encodeFunctionData(call.fragment, call.params), })); const overrides = calls.map(({ overrides }) => overrides).find(Boolean); const blockTag = DIGIT_REGEX.test(blockTagStr) ? parseInt(blockTagStr, 10) : blockTagStr; const res = yield this.multicall.callStatic .aggregate(callStructs, Object.assign(Object.assign({}, overrides), { blockTag })) .catch((error) => __awaiter(this, void 0, void 0, function* () { if (error.code === "CALL_EXCEPTION" && error.data === "0x" && error.reason == null && error.errorName == null && error.errorArgs == null) return { blockNumber: blockTag, returnData: yield Promise.all(callStructs.map((call) => __awaiter(this, void 0, void 0, function* () { return this.multicall.provider.call(Object.assign(Object.assign({}, overrides), { to: call.target, data: call.callData }), blockTag); }))), }; throw error; })); if (res.returnData.length !== calls.length) throw new Error(`Unexpected multicall response length: received ${res.returnData.length}; expected ${calls.length}`); calls.forEach((call, i) => { const signature = utils_1.FunctionFragment.from(call.fragment).format(); const callIdentifier = [call.address, signature].join(":"); try { const result = new utils_1.Interface([]).decodeFunctionResult(call.fragment, res.returnData[i]); return (results[call.index] = call.fragment.outputs.length === 1 ? result[0] : result); } catch (err) { const error = new Error(`Multicall result decoding failed for ${callIdentifier}: ${err.message}`); error.name = error.message; error.stack = call.stack; throw error; } }); }))); return results; }); } wrap(contract) { const copy = Object.setPrototypeOf(Object.assign({}, contract), Object.getPrototypeOf(contract)); copy.callStatic = Object.assign({}, contract.callStatic); copy.functions = Object.assign({}, contract.functions); const defineFunction = (property, fragment) => { const descriptor = { configurable: true, enumerable: true, writable: false, value: (...params) => { var _a; return this.dataLoader.load({ fragment, address: contract.address, stack: (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").slice(1).join("\n"), params: params.slice(0, fragment.inputs.length), overrides: params[fragment.inputs.length], }); }, }; // Overwrite the function with a dataloader batched call Object.defineProperty(copy, property, descriptor); Object.defineProperty(copy.callStatic, property, descriptor); Object.defineProperty(copy.functions, property, descriptor); }; const uniqueNames = {}; Object.entries(contract.interface.functions).forEach(([signature, fragment]) => { if (!["view", "pure"].includes(fragment.stateMutability)) return; if (!uniqueNames[`%${fragment.name}`]) uniqueNames[`%${fragment.name}`] = []; uniqueNames[`%${fragment.name}`].push(fragment); defineFunction(signature, fragment); }); Object.entries(uniqueNames).forEach(([name, fragments]) => { // Ambiguous names to not get attached as bare names if (fragments.length > 1) return; // Strip off the leading "%" used for prototype protection defineFunction(name.substring(1), fragments[0]); }); return copy; } } exports.EthersMulticall = EthersMulticall; exports.default = EthersMulticall;