import type { Signer } from '@ethersproject/abstract-signer'; import { Contract } from '@ethersproject/contracts'; import { Logger } from '@ethersproject/logger'; import { BaseProvider, BlockTag, Network, TransactionRequest } from '@ethersproject/providers'; import Multicall from './Multicall.json'; const logger = new Logger('0.1.0'); interface HasSigner { getSigner(_addressOrIndex?: string | number): Signer; } type BatchCallItem = { // eslint-disable-next-line @typescript-eslint/no-explicit-any request: { target: string; callData: any }; // eslint-disable-next-line @typescript-eslint/no-explicit-any resolve: (_result: any) => void; reject: (_error: Error) => void; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function hasSigner(obj: any): obj is HasSigner { return (obj as unknown as HasSigner).getSigner !== undefined; } type CallParams = { transaction: TransactionRequest; blockTag?: BlockTag }; // Multicall3 is deployed at the same create2 address on basically every chain // https://github.com/mds1/multicall const multicall3Address = '0xcA11bde05977b3631167028862bE2a173976CA11'; export class JsonRpcMulticallProvider extends BaseProvider { readonly parent: BaseProvider; _pendingBatchAggregator?: NodeJS.Timer | null; _pendingBatch?: Array<BatchCallItem> | null; constructor(provider: BaseProvider) { super(provider.getNetwork()); this.parent = provider; } getSigner(addressOrIndex?: string | number): Signer { if (!hasSigner(this.parent)) { return logger.throwError('Parent provider does not support getSigner', Logger.errors.NOT_IMPLEMENTED, { parent: this.parent, }); } return this.parent.getSigner(addressOrIndex); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async perform(method: string, params: any): Promise<any> { if (method === 'call') { const reqParams = params as CallParams; const target = reqParams.transaction.to; const callData = reqParams.transaction.data; // If there is no call data or unknown multicall contract, just passthrough to parent if (!target || !callData) { return this.parent.perform(method, params); } if (!this._pendingBatch) { this._pendingBatch = []; } const newCall: BatchCallItem = { request: { target, callData }, resolve: null!, reject: null! }; const promise = new Promise((resolve, reject) => { newCall.resolve = resolve; newCall.reject = reject; }); this._pendingBatch.push(newCall); if (!this._pendingBatchAggregator) { const provider = this.parent; this._pendingBatchAggregator = setTimeout(async () => { const batch = this._pendingBatch; if (!batch) { return; } this._pendingBatch = null; this._pendingBatchAggregator = null; const multicall = new Contract(multicall3Address, Multicall.abi, provider); // returns [blockNumber, call results], so results are at index 1 const multicallResult = await multicall.aggregate(batch.map(i => i.request)); // eslint-disable-next-line @typescript-eslint/no-explicit-any multicallResult[1].map((result: any, i: number) => batch[i].resolve(result)); this._pendingBatch = null; this._pendingBatchAggregator = null; }, 10); } return promise; } else { return this.parent.perform(method, params); } } detectNetwork(): Promise<Network> { return this.parent.detectNetwork(); } }