@morpho-labs/ethers-multicall
Version:
⚡🚀 Call multiple view functions, from multiple Smart Contracts, in a single RPC query!
142 lines (141 loc) • 8 kB
JavaScript
;
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;