From fcd082846c1debf8a8c3397a9c48f871ae979c26 Mon Sep 17 00:00:00 2001 From: Karan Shahani Date: Thu, 5 May 2022 10:35:18 -0400 Subject: [PATCH 1/6] feat: Use dataloader in multicall wrapper --- package.json | 4 + pnpm-lock.yaml | 17 ++ src/multicall/multicall.contract.ts | 68 ++++++++ src/multicall/multicall.ethers.ts | 256 ++++++++-------------------- src/multicall/multicall.utils.ts | 45 +++++ 5 files changed, 202 insertions(+), 188 deletions(-) create mode 100644 src/multicall/multicall.contract.ts create mode 100644 src/multicall/multicall.utils.ts diff --git a/package.json b/package.json index 995000470..5b8cd243a 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "class-validator": "^0.13.2", "conventional-changelog-conventionalcommits": "^4.6.3", "copyfiles": "^2.4.1", + "dataloader": "^2.1.0", "dedent": "^0.7.0", "dotenv": "^16.0.0", "eslint": "^8.11.0", @@ -71,6 +72,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-unused-imports": "^2.0.0", "ethers": "^5.5.1", + "ethers-multicall": "^0.2.3", "graphql": "^15.5.1", "graphql-request": "^3.7.0", "jest": "^27.5.1", @@ -105,7 +107,9 @@ "cache-manager": "^3.4.1", "class-transformer": "^0.4.0", "class-validator": "^0.13.2", + "dataloader": "^2.1.0", "ethers": "^5.5.1", + "ethers-multicall": "^0.2.3", "graphql": "14 || 15 || 16", "graphql-request": "^3", "moment": "^2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed16a1e2b..7861aa43a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,7 @@ specifiers: class-validator: ^0.13.2 conventional-changelog-conventionalcommits: ^4.6.3 copyfiles: ^2.4.1 + dataloader: ^2.1.0 dedent: ^0.7.0 dotenv: ^16.0.0 esbuild: ^0.14.27 @@ -44,6 +45,7 @@ specifiers: eslint-plugin-prettier: ^4.0.0 eslint-plugin-unused-imports: ^2.0.0 ethers: ^5.5.1 + ethers-multicall: ^0.2.3 file-system-cache: ^1.0.5 fs-extra: ^10.0.1 graphql: ^15.5.1 @@ -117,6 +119,7 @@ devDependencies: class-validator: 0.13.2 conventional-changelog-conventionalcommits: 4.6.3 copyfiles: 2.4.1 + dataloader: 2.1.0 dedent: 0.7.0 dotenv: 16.0.0 eslint: 8.11.0 @@ -124,6 +127,7 @@ devDependencies: eslint-plugin-prettier: 4.0.0_eslint@8.11.0+prettier@2.6.0 eslint-plugin-unused-imports: 2.0.0_0d92ebcd20257249efc95aa75e3847d0 ethers: 5.6.2 + ethers-multicall: 0.2.3 graphql: 15.8.0 graphql-request: 3.7.0_graphql@15.8.0 jest: 27.5.1_ts-node@10.7.0 @@ -3181,6 +3185,10 @@ packages: whatwg-url: 8.7.0 dev: true + /dataloader/2.1.0: + resolution: {integrity: sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==} + dev: true + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} dependencies: @@ -4022,6 +4030,15 @@ packages: rlp: 2.2.7 dev: true + /ethers-multicall/0.2.3: + resolution: {integrity: sha512-RaWQuLy+HzeKOibptlc9RZ6j7bT1H6VnkdAKTHiLx2t/lpyfS2ckXHdQhhRbCaXNc1iu6CgoisgMejxKHg84tg==} + dependencies: + ethers: 5.6.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /ethers/5.6.2: resolution: {integrity: sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ==} dependencies: diff --git a/src/multicall/multicall.contract.ts b/src/multicall/multicall.contract.ts new file mode 100644 index 000000000..f2aae019e --- /dev/null +++ b/src/multicall/multicall.contract.ts @@ -0,0 +1,68 @@ +import { Fragment, FunctionFragment, JsonFragment } from '@ethersproject/abi'; + +export class MulticallContract { + private _address: string; + private _abi: Fragment[]; + private _functions: FunctionFragment[]; + + get address() { + return this._address; + } + + get abi() { + return this._abi; + } + + get functions() { + return this._functions; + } + + constructor(address: string, abi: JsonFragment[] | string[] | Fragment[]) { + this._address = address; + + this._abi = toFragment(abi); + + this._functions = this._abi.filter(x => x.type === 'function').map(x => FunctionFragment.from(x)); + const callFunctions = this._functions.filter(x => x.stateMutability === 'pure' || x.stateMutability === 'view'); + + for (const callFunction of callFunctions) { + const { name } = callFunction; + const getCall = makeCallFunction(this, name); + if (!this[name]) { + defineReadOnly(this, name, getCall); + } + } + } + + [method: string]: any; +} + +function toFragment(abi: JsonFragment[] | string[] | Fragment[]): Fragment[] { + return abi.map((item: JsonFragment | string | Fragment) => Fragment.from(item)); +} + +function makeCallFunction(contract: MulticallContract, name: string) { + return (...params: any[]) => { + const { address } = contract; + const { inputs } = contract.functions.find(f => f.name === name)!; + const { outputs } = contract.functions.find(f => f.name === name)!; + + return { + contract: { + address, + }, + name, + inputs, + outputs, + params, + }; + }; +} + +function defineReadOnly(object: object, name: string, value: unknown) { + Object.defineProperty(object, name, { + enumerable: true, + value, + writable: false, + }); +} diff --git a/src/multicall/multicall.ethers.ts b/src/multicall/multicall.ethers.ts index c0ab97cbf..cd413ae40 100644 --- a/src/multicall/multicall.ethers.ts +++ b/src/multicall/multicall.ethers.ts @@ -1,206 +1,86 @@ -import { FunctionFragment } from '@ethersproject/abi'; -import { Contract } from '@ethersproject/contracts'; -import { ethers } from 'ethers'; - -import { Multicall as MulticallContract } from '~contract/contracts'; - -export type EthersMulticallConfig = { - batchInterval?: number; - batchMaxSize?: number; -}; - -type MulticallAggregateReturnData = Awaited>['returnData']; -type TargetContract = Pick; -type HasContractFunctions = { functions: Record }; - -// Extracts the contract functions in `functions` -type AllContractFunctions = keyof { - [P in keyof T['functions']]: T[P]; -}; - -// Extracts all methods from type -type Methods = { [P in keyof T as T[P] extends (...args: any[]) => any ? P : never]: T[P] }; - -// Picks only the methods on the contract that are defined in `functions` -type ExtractContractFunctions = Pick, AllContractFunctions>; - -type BatchItem = { - callData: string; - callTarget: string; - functionFragment: FunctionFragment; - resolve: (value?: any) => void; - reject: (reason?: any) => void; +import DataLoader from 'dataloader'; +import { Contract } from 'ethers'; +import { FunctionFragment, ParamType } from 'ethers/lib/utils'; + +import { Multicall } from '~contract/contracts'; + +import { MulticallContract } from './multicall.contract'; +import { Abi } from './multicall.utils'; + +export type ContractCall = { + contract: { + address: string; + }; + name: string; + inputs: ParamType[]; + outputs: ParamType[]; + params: any[]; }; export class EthersMulticall { - private readonly batchInterval: number; - private readonly batchMaxSize: number; - private readonly multicallContract: MulticallContract; - private batchRequests: BatchItem[] | null; - - constructor(multicallContact: MulticallContract, opts: EthersMulticallConfig = {}) { - const { batchInterval = 10, batchMaxSize = 250 } = opts; - - this.multicallContract = multicallContact; - this.batchInterval = batchInterval; - this.batchMaxSize = batchMaxSize; - this.batchRequests = null; - } - - get contract() { - return this.multicallContract; - } - - /** - * Intercepts provider calls to functions / static functions on the contract, - * replaces execution with encoding the function data of the target & args, then - * adds the function data to the multicall consumption queue - */ - private hijackExecution(contract: TargetContract, method: string) { - return (...args: any[]) => { - const functionFragment = contract.interface.getFunction(method as string); - if (!functionFragment) throw new Error('Cannot find function on the given contract'); - - if (!this.batchRequests) { - this.batchRequests = []; - } - - return new Promise((resolve, reject) => { - this.batchRequests?.push({ - functionFragment, - callData: contract.interface.encodeFunctionData(functionFragment, args), - callTarget: contract.address, - resolve, - reject, - }); - - if (this.batchRequests?.length === 1) { - this.scheduleConsumeQueue(); - } - - if (this.batchRequests?.length === this.batchMaxSize) { - this.consumeQueue(); - } - }); - }; + private multicall: Multicall; + private dataLoader: DataLoader; + + constructor( + multicall: Multicall, + dataLoaderOptions: DataLoader.Options = { cache: false, maxBatchSize: 250 }, + ) { + this.multicall = multicall; + this.dataLoader = new DataLoader(this.doCalls.bind(this), dataLoaderOptions); } - wrap(contract: T): ExtractContractFunctions & Pick { - // Removes readonly contraints on the contract properties - const configurableContract = Object.create(contract); - - return new Proxy(configurableContract, { - get: (target, key) => { - const functionName = key as string; - const isFunctionCall = functionName in configurableContract.functions; - const isStaticFunctionCall = functionName === 'callStatic'; - // Disregard calls other than provider method invocations - if (!isFunctionCall && !isStaticFunctionCall) throw new Error('Invalid multicall operation'); - - if (isStaticFunctionCall) { - // Removes readonly contraints on the `functions` / `callStatic` properties - const configurableTarget = Object.create(target[functionName]); - - return new Proxy(configurableTarget, { - get: (_target, staticFunctionName) => this.hijackExecution(contract, staticFunctionName as string), - }); - } - - return this.hijackExecution(contract, functionName); - }, - }); - } + private async doCalls(calls: readonly ContractCall[]) { + const callRequests = calls.map(call => ({ + target: call.contract.address, + callData: Abi.encode(call.name, call.inputs, call.params), + })); - private async aggregate(batchItems: BatchItem[]) { - // Prepare batch items into payloads - const calls = batchItems.map(({ callData, callTarget }) => { - return { - target: callTarget, - callData, - }; - }); + const response = await this.multicall.callStatic.aggregate(callRequests, false); - // Actual call to multicall's aggregate - try { - const responses = await this.multicallContract.callStatic.aggregate(calls, false); - const returnData = responses.returnData; + const result = calls.map((call, i) => { + const signature = FunctionFragment.from(call).format(); + const callIdentifier = [call.contract.address, signature].join(':'); + const [success, data] = response.returnData[i]; - if (returnData.length !== batchItems.length) { - throw new Error(`Unexpected response length: received ${returnData.length}; expected ${batchItems.length}`); + if (!success) { + return new Error(`Multicall call failed for ${callIdentifier}`); } - return returnData; - } catch (err) { - const exception = err as Error; - exception.message = `Multicall aggregate request failed: ${exception.message}`; - throw err; - } - } - - private decodeFunctionData(batchItem: BatchItem, batchReturnData: MulticallAggregateReturnData[number]) { - const { callTarget, functionFragment } = batchItem; - const [success, data] = batchReturnData; - const functionSignature = functionFragment.format(); - - // Multicall's response for the batch item failed - if (!success) { - const callIdentifier = [callTarget, functionSignature].join(':'); - throw new Error(`Multicall call failed for ${callIdentifier}`); - } - - try { - const decoder = ethers.utils.defaultAbiCoder; - if (!functionFragment.outputs) throw new Error('no outputs received'); - const decoded = decoder.decode(functionFragment.outputs, data); - if (functionFragment.outputs?.length > 1) return decoded; - return decoded[0]; - } catch (err) { - const exception = err as Error; - const callIdentifier = [callTarget, functionSignature].join(':'); - exception.message = `Multicall call failed for ${callIdentifier}: ${exception.message}`; - throw err; - } - } - - private async consumeQueue() { - const batch = this.batchRequests; - if (!batch) return; - - // Clear the batch queue - this.batchRequests = null; - - // Call multicall's aggregate with all the batch requests - let returnData: MulticallAggregateReturnData; - try { - returnData = await this.aggregate(batch); - } catch (err) { - // Each batch rejects with the error reason - batch.forEach(({ reject }) => reject(err)); - return; - } - - // Decode the data for each batch item - batch.forEach((batchItem, i) => { - const { resolve, reject } = batchItem; try { - const decodedBatchData = this.decodeFunctionData(batchItem, returnData[i]); - resolve(decodedBatchData); + const outputs = call.outputs; + const params = Abi.decode(outputs, data); + return outputs.length === 1 ? params[0] : params; } catch (err) { - reject(err); + return new Error(`Multicall call failed for ${callIdentifier}`); } }); + + return result; } - private scheduleConsumeQueue(): void { - setTimeout(async () => { - if (this.batchRequests?.length) { - try { - await this.consumeQueue(); - } catch (err) { - const exception = err as Error; - exception.message = `Multicall unexpected error occurred: ${exception.message}`; - } - } - }, this.batchInterval); + wrap(contract: T) { + const abi = contract.interface.fragments; + const multicallContract = new MulticallContract(contract.address, abi as any); + const dataLoader = this.dataLoader; + + const funcs = abi.reduce((memo, frag) => { + if (frag.type !== 'function') return memo; + + const funcFrag = frag as FunctionFragment; + if (!['pure', 'view'].includes(funcFrag.stateMutability)) return memo; + + // Overwrite the function with a dataloader batched call + const multicallFunc = multicallContract[funcFrag.name].bind(multicallContract); + const newFunc = (...args: any) => { + const contractCall = multicallFunc(...args); + return dataLoader.load(contractCall); + }; + + memo[funcFrag.name] = newFunc; + }, {} as Record any>); + + return Object.setPrototypeOf({ ...contract, ...funcs }, Contract.prototype) as any as T; } } + +export default EthersMulticall; diff --git a/src/multicall/multicall.utils.ts b/src/multicall/multicall.utils.ts new file mode 100644 index 000000000..1debefddf --- /dev/null +++ b/src/multicall/multicall.utils.ts @@ -0,0 +1,45 @@ +import { AbiCoder, BytesLike, keccak256, ParamType, toUtf8Bytes } from 'ethers/lib/utils'; + +export class Abi { + public static encode(name: string, inputs: ParamType[], params: any[]) { + const functionSignature = getFunctionSignature(name, inputs); + const functionHash = keccak256(toUtf8Bytes(functionSignature)); + const functionData = functionHash.substring(2, 10); + const abiCoder = new AbiCoder(); + const argumentString = abiCoder.encode(inputs, params); + const argumentData = argumentString.substring(2); + const inputData = `0x${functionData}${argumentData}`; + return inputData; + } + + public static decode(outputs: ParamType[], data: BytesLike) { + const abiCoder = new AbiCoder(); + const params = abiCoder.decode(outputs, data); + return params; + } +} + +function getFunctionSignature(name: string, inputs: ParamType[]) { + const types: string[] = []; + + for (const input of inputs) { + if (input.type === 'tuple') { + const tupleString = getFunctionSignature('', input.components); + types.push(tupleString); + continue; + } + + if (input.type === 'tuple[]') { + const tupleString = getFunctionSignature('', input.components); + const arrayString = `${tupleString}[]`; + types.push(arrayString); + continue; + } + + types.push(input.type); + } + + const typeString = types.join(','); + const functionSignature = `${name}(${typeString})`; + return functionSignature; +} From 2b9b8f949ea36d3c2b8d5154161dc2743ca5f3f7 Mon Sep 17 00:00:00 2001 From: Karan Shahani Date: Sat, 7 May 2022 12:07:10 -0400 Subject: [PATCH 2/6] Done --- src/multicall/multicall.contract.ts | 5 +++-- src/multicall/multicall.ethers.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/multicall/multicall.contract.ts b/src/multicall/multicall.contract.ts index f2aae019e..b0721f0e0 100644 --- a/src/multicall/multicall.contract.ts +++ b/src/multicall/multicall.contract.ts @@ -44,10 +44,11 @@ function toFragment(abi: JsonFragment[] | string[] | Fragment[]): Fragment[] { function makeCallFunction(contract: MulticallContract, name: string) { return (...params: any[]) => { const { address } = contract; - const { inputs } = contract.functions.find(f => f.name === name)!; - const { outputs } = contract.functions.find(f => f.name === name)!; + const { type, stateMutability, inputs, outputs } = contract.functions.find(f => f.name === name)!; return { + type, + stateMutability, contract: { address, }, diff --git a/src/multicall/multicall.ethers.ts b/src/multicall/multicall.ethers.ts index cd413ae40..6cbd680f7 100644 --- a/src/multicall/multicall.ethers.ts +++ b/src/multicall/multicall.ethers.ts @@ -48,8 +48,8 @@ export class EthersMulticall { try { const outputs = call.outputs; - const params = Abi.decode(outputs, data); - return outputs.length === 1 ? params[0] : params; + const result = Abi.decode(outputs, data); + return outputs.length === 1 ? result[0] : result; } catch (err) { return new Error(`Multicall call failed for ${callIdentifier}`); } @@ -77,6 +77,7 @@ export class EthersMulticall { }; memo[funcFrag.name] = newFunc; + return memo; }, {} as Record any>); return Object.setPrototypeOf({ ...contract, ...funcs }, Contract.prototype) as any as T; From ad6dc07574e78c06475c6ce7a04633496451e9c9 Mon Sep 17 00:00:00 2001 From: Karan Shahani Date: Sat, 7 May 2022 22:01:18 -0400 Subject: [PATCH 3/6] Simplify --- src/multicall/multicall.contract.ts | 32 +++++--------------- src/multicall/multicall.ethers.ts | 23 ++++++--------- src/multicall/multicall.utils.ts | 45 ----------------------------- 3 files changed, 17 insertions(+), 83 deletions(-) delete mode 100644 src/multicall/multicall.utils.ts diff --git a/src/multicall/multicall.contract.ts b/src/multicall/multicall.contract.ts index b0721f0e0..4b070583d 100644 --- a/src/multicall/multicall.contract.ts +++ b/src/multicall/multicall.contract.ts @@ -1,5 +1,7 @@ import { Fragment, FunctionFragment, JsonFragment } from '@ethersproject/abi'; +import { ContractCall } from './multicall.ethers'; + export class MulticallContract { private _address: string; private _abi: Fragment[]; @@ -20,50 +22,32 @@ export class MulticallContract { constructor(address: string, abi: JsonFragment[] | string[] | Fragment[]) { this._address = address; - this._abi = toFragment(abi); - + this._abi = abi.map((item: JsonFragment | string | Fragment) => Fragment.from(item)); this._functions = this._abi.filter(x => x.type === 'function').map(x => FunctionFragment.from(x)); const callFunctions = this._functions.filter(x => x.stateMutability === 'pure' || x.stateMutability === 'view'); for (const callFunction of callFunctions) { const { name } = callFunction; const getCall = makeCallFunction(this, name); - if (!this[name]) { - defineReadOnly(this, name, getCall); - } + if (!this[name]) defineReadOnly(this, name, getCall); } } [method: string]: any; } -function toFragment(abi: JsonFragment[] | string[] | Fragment[]): Fragment[] { - return abi.map((item: JsonFragment | string | Fragment) => Fragment.from(item)); -} - function makeCallFunction(contract: MulticallContract, name: string) { - return (...params: any[]) => { + return (...params: any[]): ContractCall => { const { address } = contract; - const { type, stateMutability, inputs, outputs } = contract.functions.find(f => f.name === name)!; - - return { - type, - stateMutability, - contract: { - address, - }, - name, - inputs, - outputs, - params, - }; + const fragment = contract.functions.find(f => f.name === name)!; + return { fragment, address, params }; }; } function defineReadOnly(object: object, name: string, value: unknown) { Object.defineProperty(object, name, { enumerable: true, - value, writable: false, + value, }); } diff --git a/src/multicall/multicall.ethers.ts b/src/multicall/multicall.ethers.ts index 6cbd680f7..71c092e90 100644 --- a/src/multicall/multicall.ethers.ts +++ b/src/multicall/multicall.ethers.ts @@ -1,19 +1,14 @@ import DataLoader from 'dataloader'; import { Contract } from 'ethers'; -import { FunctionFragment, ParamType } from 'ethers/lib/utils'; +import { FunctionFragment, Interface } from 'ethers/lib/utils'; import { Multicall } from '~contract/contracts'; import { MulticallContract } from './multicall.contract'; -import { Abi } from './multicall.utils'; export type ContractCall = { - contract: { - address: string; - }; - name: string; - inputs: ParamType[]; - outputs: ParamType[]; + fragment: FunctionFragment; + address: string; params: any[]; }; @@ -31,15 +26,15 @@ export class EthersMulticall { private async doCalls(calls: readonly ContractCall[]) { const callRequests = calls.map(call => ({ - target: call.contract.address, - callData: Abi.encode(call.name, call.inputs, call.params), + target: call.address, + callData: new Interface([]).encodeFunctionData(call.fragment, call.params), })); const response = await this.multicall.callStatic.aggregate(callRequests, false); const result = calls.map((call, i) => { - const signature = FunctionFragment.from(call).format(); - const callIdentifier = [call.contract.address, signature].join(':'); + const signature = FunctionFragment.from(call.fragment).format(); + const callIdentifier = [call.address, signature].join(':'); const [success, data] = response.returnData[i]; if (!success) { @@ -47,8 +42,8 @@ export class EthersMulticall { } try { - const outputs = call.outputs; - const result = Abi.decode(outputs, data); + const outputs = call.fragment.outputs!; + const result = new Interface([]).decodeFunctionResult(call.fragment, data); return outputs.length === 1 ? result[0] : result; } catch (err) { return new Error(`Multicall call failed for ${callIdentifier}`); diff --git a/src/multicall/multicall.utils.ts b/src/multicall/multicall.utils.ts deleted file mode 100644 index 1debefddf..000000000 --- a/src/multicall/multicall.utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AbiCoder, BytesLike, keccak256, ParamType, toUtf8Bytes } from 'ethers/lib/utils'; - -export class Abi { - public static encode(name: string, inputs: ParamType[], params: any[]) { - const functionSignature = getFunctionSignature(name, inputs); - const functionHash = keccak256(toUtf8Bytes(functionSignature)); - const functionData = functionHash.substring(2, 10); - const abiCoder = new AbiCoder(); - const argumentString = abiCoder.encode(inputs, params); - const argumentData = argumentString.substring(2); - const inputData = `0x${functionData}${argumentData}`; - return inputData; - } - - public static decode(outputs: ParamType[], data: BytesLike) { - const abiCoder = new AbiCoder(); - const params = abiCoder.decode(outputs, data); - return params; - } -} - -function getFunctionSignature(name: string, inputs: ParamType[]) { - const types: string[] = []; - - for (const input of inputs) { - if (input.type === 'tuple') { - const tupleString = getFunctionSignature('', input.components); - types.push(tupleString); - continue; - } - - if (input.type === 'tuple[]') { - const tupleString = getFunctionSignature('', input.components); - const arrayString = `${tupleString}[]`; - types.push(arrayString); - continue; - } - - types.push(input.type); - } - - const typeString = types.join(','); - const functionSignature = `${name}(${typeString})`; - return functionSignature; -} From a1f45f1e5e1d1e4b3a09e434c39c06741b388c3b Mon Sep 17 00:00:00 2001 From: Karan Shahani Date: Sat, 7 May 2022 22:02:07 -0400 Subject: [PATCH 4/6] Remove unnecessary package --- package.json | 2 -- pnpm-lock.yaml | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/package.json b/package.json index 5b8cd243a..40867589e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-unused-imports": "^2.0.0", "ethers": "^5.5.1", - "ethers-multicall": "^0.2.3", "graphql": "^15.5.1", "graphql-request": "^3.7.0", "jest": "^27.5.1", @@ -109,7 +108,6 @@ "class-validator": "^0.13.2", "dataloader": "^2.1.0", "ethers": "^5.5.1", - "ethers-multicall": "^0.2.3", "graphql": "14 || 15 || 16", "graphql-request": "^3", "moment": "^2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7861aa43a..a148950b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,7 +45,6 @@ specifiers: eslint-plugin-prettier: ^4.0.0 eslint-plugin-unused-imports: ^2.0.0 ethers: ^5.5.1 - ethers-multicall: ^0.2.3 file-system-cache: ^1.0.5 fs-extra: ^10.0.1 graphql: ^15.5.1 @@ -127,7 +126,6 @@ devDependencies: eslint-plugin-prettier: 4.0.0_eslint@8.11.0+prettier@2.6.0 eslint-plugin-unused-imports: 2.0.0_0d92ebcd20257249efc95aa75e3847d0 ethers: 5.6.2 - ethers-multicall: 0.2.3 graphql: 15.8.0 graphql-request: 3.7.0_graphql@15.8.0 jest: 27.5.1_ts-node@10.7.0 @@ -4030,15 +4028,6 @@ packages: rlp: 2.2.7 dev: true - /ethers-multicall/0.2.3: - resolution: {integrity: sha512-RaWQuLy+HzeKOibptlc9RZ6j7bT1H6VnkdAKTHiLx2t/lpyfS2ckXHdQhhRbCaXNc1iu6CgoisgMejxKHg84tg==} - dependencies: - ethers: 5.6.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: true - /ethers/5.6.2: resolution: {integrity: sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ==} dependencies: From c1a96a42d8729afa40f160f9b907a6c7e47c05c0 Mon Sep 17 00:00:00 2001 From: Karan Shahani Date: Sat, 7 May 2022 22:06:57 -0400 Subject: [PATCH 5/6] Elegance --- src/multicall/multicall.contract.ts | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/multicall/multicall.contract.ts b/src/multicall/multicall.contract.ts index 4b070583d..53e9ab57c 100644 --- a/src/multicall/multicall.contract.ts +++ b/src/multicall/multicall.contract.ts @@ -24,30 +24,13 @@ export class MulticallContract { this._abi = abi.map((item: JsonFragment | string | Fragment) => Fragment.from(item)); this._functions = this._abi.filter(x => x.type === 'function').map(x => FunctionFragment.from(x)); - const callFunctions = this._functions.filter(x => x.stateMutability === 'pure' || x.stateMutability === 'view'); + const fragments = this._functions.filter(x => x.stateMutability === 'pure' || x.stateMutability === 'view'); - for (const callFunction of callFunctions) { - const { name } = callFunction; - const getCall = makeCallFunction(this, name); - if (!this[name]) defineReadOnly(this, name, getCall); + for (const frag of fragments) { + const fn = (...params: any[]): ContractCall => ({ fragment: frag, address, params }); + if (!this[frag.name]) Object.defineProperty(this, frag.name, { enumerable: true, writable: false, value: fn }); } } [method: string]: any; } - -function makeCallFunction(contract: MulticallContract, name: string) { - return (...params: any[]): ContractCall => { - const { address } = contract; - const fragment = contract.functions.find(f => f.name === name)!; - return { fragment, address, params }; - }; -} - -function defineReadOnly(object: object, name: string, value: unknown) { - Object.defineProperty(object, name, { - enumerable: true, - writable: false, - value, - }); -} From a6882bcd6f83c0e09088523255fa2cdb8f6b075b Mon Sep 17 00:00:00 2001 From: Karan Shahani Date: Sat, 7 May 2022 22:11:28 -0400 Subject: [PATCH 6/6] OCD --- src/multicall/multicall.ethers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/multicall/multicall.ethers.ts b/src/multicall/multicall.ethers.ts index 71c092e90..96cfb6f87 100644 --- a/src/multicall/multicall.ethers.ts +++ b/src/multicall/multicall.ethers.ts @@ -56,7 +56,6 @@ export class EthersMulticall { wrap(contract: T) { const abi = contract.interface.fragments; const multicallContract = new MulticallContract(contract.address, abi as any); - const dataLoader = this.dataLoader; const funcs = abi.reduce((memo, frag) => { if (frag.type !== 'function') return memo; @@ -68,7 +67,7 @@ export class EthersMulticall { const multicallFunc = multicallContract[funcFrag.name].bind(multicallContract); const newFunc = (...args: any) => { const contractCall = multicallFunc(...args); - return dataLoader.load(contractCall); + return this.dataLoader.load(contractCall); }; memo[funcFrag.name] = newFunc;