Skip to content

Commit

Permalink
Merge pull request #50 from Rubilmax/ethers-v6
Browse files Browse the repository at this point in the history
Ethers v6
  • Loading branch information
Rubilmax authored Aug 28, 2023
2 parents 87e02e7 + 93a9054 commit 6680f52
Show file tree
Hide file tree
Showing 14 changed files with 1,170 additions and 1,936 deletions.
1 change: 0 additions & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- main
- next

jobs:
test:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ on:
jobs:
jest:
runs-on: ubuntu-latest
if: github.head_ref != 'next' # already triggered by push

strategy:
matrix:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,16 @@ Promise.all([uni.name(), uni.symbol()]).then(console.log);

## Limits

### `msg.sender` override

Because calls are batched through the Multicall contract, all calls will inherently have the Multicall contract as `msg.sender`. This has no impact on most queries, because most of the time `msg.sender` is not used in view functions ; but it may introduce unexpected behaviors in specific smart contracts.

To circumvent this, just use the default ethers provider in places where you don't want `msg.sender` to be overriden.

### Network cache

Starting from `ethers-v6`, network is no longer cached in the provider, so that each RPC call first requests the network and updates the provider consequently. Using `ethers-multicall-provider`, the first network the provider is connected to is cached and can only be changed by calling `fetchNetwork()`.

[build-img]: https://github.com/rubilmax/ethers-multicall-provider/actions/workflows/release.yml/badge.svg
[build-url]: https://github.com/rubilmax/ethers-multicall-provider/actions/workflows/release.yml
[downloads-img]: https://img.shields.io/npm/dt/ethers-multicall-provider
Expand Down
25 changes: 10 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "ethers-multicall-provider",
"version": "1.0.0",
"version": "4.0.0",
"description": "⚡🕰️ Drop-in solution to batch smart contract RPC calls in a single RPC query via Multicall!",
"main": "lib/index.js",
"type": "module",
"files": [
"lib/*"
],
Expand All @@ -12,7 +13,7 @@
"test:watch": "jest --watch",
"test": "jest --coverage --detectOpenHandles",
"typecheck": "tsc --noEmit",
"typechain": "typechain --target ethers-v5 --out-dir src/types 'abis/*.json'"
"typechain": "typechain --target ethers-v6 --out-dir src/types 'abis/*.json'"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -41,33 +42,28 @@
},
"homepage": "https://github.com/rubilmax/ethers-multicall-provider#readme",
"dependencies": {
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"ethers": "^5.0.0",
"ethers": "^6.0.0",
"lodash": "^4.17.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@typechain/ethers-v5": "^11.1.1",
"@types/jest": "^29.5.3",
"@typechain/ethers-v6": "^0.5.0",
"@types/jest": "^29.5.4",
"@types/lodash": "^4.14.197",
"@types/lodash.debounce": "^4.0.7",
"commitizen": "^4.3.0",
"conventional-changelog-conventionalcommits": "^6.1.0",
"cz-conventional-changelog": "^3.3.0",
"dotenv": "^16.3.1",
"husky": "^8.0.3",
"jest": "^29.6.3",
"jest": "^29.6.4",
"lint-staged": "^14.0.1",
"prettier": "^2.8.8",
"ts-jest": "^29.1.1",
"typechain": "^8.3.1",
"typescript": "^5.1.6"
},
"peerDependencies": {
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"ethers": "^5.0.0",
"lodash": "^4.17.0"
},
"config": {
Expand All @@ -80,8 +76,7 @@
},
"release": {
"branches": [
"main",
"next"
"main"
],
"plugins": [
[
Expand Down Expand Up @@ -127,7 +122,7 @@
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testTimeout": 30000,
"testTimeout": 60000,
"testMatch": [
"<rootDir>/test/**/*.spec.ts"
],
Expand All @@ -145,4 +140,4 @@
]
}
}
}
}
67 changes: 42 additions & 25 deletions src/multicall-provider.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { BytesLike } from "ethers/lib/utils";
import { BlockTag, BytesLike, AbstractProvider, PerformActionRequest, Network } from "ethers";
import { DebouncedFunc } from "lodash";
import _debounce from "lodash/debounce";

import { Provider, BlockTag, TransactionRequest } from "@ethersproject/providers";

import { multicallAddresses } from "./constants";
import { Multicall2, Multicall3 } from "./types";
import { MinimalProvider, getBlockNumber, getMulticall, isProviderCompatible } from "./utils";
import { getBlockNumber, getMulticall } from "./utils";

export interface ContractCall {
export interface ContractCall<T = any> {
to: string;
data: BytesLike;
blockTag: BlockTag;
multicall: Multicall2 | Multicall3;
resolve: (value: string | PromiseLike<string>) => void;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}

export type MulticallProvider<T extends Provider = Provider> = T & {
export type MulticallProvider<T extends AbstractProvider = AbstractProvider> = T & {
readonly _isMulticallProvider: boolean;

fetchNetwork(): Promise<Network>;
_networkPromise: Promise<Network>;

_multicallDelay: number;
multicallDelay: number;
maxMulticallDataLength: number;
Expand All @@ -35,7 +36,7 @@ export class MulticallWrapper {
* @param provider The provider to check.
* @returns A boolean indicating whether the given provider is a multicall-enabled provider.
*/
public static isMulticallProvider<T extends Provider>(
public static isMulticallProvider<T extends AbstractProvider>(
provider: T
): provider is MulticallProvider<T> {
if ((provider as MulticallProvider<T>)._isMulticallProvider) return true;
Expand All @@ -50,12 +51,11 @@ export class MulticallWrapper {
* @param maxMulticallDataLength The maximum total calldata length allowed in a multicall batch, to avoid having the RPC backend to revert because of too large (or too long) request. Set to 0 to disable this behavior. Defaults to 200k.
* @returns The multicall provider, which is a proxy to the given provider, automatically batching any call performed with it.
*/
public static wrap<T extends Provider>(
public static wrap<T extends AbstractProvider>(
provider: T,
delay = 16,
maxMulticallDataLength = 200_000
): MulticallProvider<T> {
if (!isProviderCompatible(provider)) throw Error("Cannot wrap provider for multicall");
if (MulticallWrapper.isMulticallProvider(provider)) return provider; // Do not overwrap when given provider is already a multicall provider.

// Overload provider
Expand Down Expand Up @@ -101,7 +101,7 @@ export class MulticallWrapper {
},
});

const multicallProvider = provider as MulticallProvider<T & MinimalProvider>;
const multicallProvider = provider as MulticallProvider<T>;

// Define execution context

Expand All @@ -121,7 +121,7 @@ export class MulticallWrapper {
...acc,
[blockTag]: [queuedCall].concat(acc[blockTag] ?? []),
};
}, {} as { [blockTag: BlockTag]: ContractCall[] });
}, {} as { [blockTag: string]: ContractCall[] });

await Promise.all(
Object.values(blockTagCalls).map(async (blockTagQueuedCalls) => {
Expand Down Expand Up @@ -151,7 +151,7 @@ export class MulticallWrapper {
try {
const res = (
await Promise.all(
calls.map((call) => multicall.callStatic.tryAggregate(false, call, { blockTag }))
calls.map((call) => multicall.tryAggregate.staticCall(false, call, { blockTag }))
)
).flat();

Expand All @@ -176,30 +176,47 @@ export class MulticallWrapper {

multicallProvider.multicallDelay = delay;

// Overload `BaseProvider.perform`
// Expose `Provider.fetchNetwork` to fetch & update the network cache when needed

const getNetwork = provider.getNetwork.bind(provider);

multicallProvider.fetchNetwork = async function fetchNetwork(): Promise<Network> {
this._networkPromise = getNetwork();

return this._networkPromise;
};

multicallProvider.fetchNetwork();

// Overload `Provider._detectNetwork` to disable polling the network at each RPC call

multicallProvider._detectNetwork = async function _detectNetwork(): Promise<Network> {
return this._networkPromise;
};

// Overload `Provider._perform`

const _perform = provider.perform.bind(provider);
const _perform = provider._perform.bind(provider);

multicallProvider.perform = async function (method: string, params: any): Promise<string> {
if (method !== "call" || !this.isMulticallEnabled) return _perform(method, params);
multicallProvider._perform = async function <R = any>(req: PerformActionRequest): Promise<R> {
if (req.method !== "call" || !this.isMulticallEnabled) return _perform(req);

const {
transaction: { to, data },
blockTag,
} = params as {
transaction: TransactionRequest;
blockTag: BlockTag;
};
} = req;

const network = await this._networkPromise;

const blockNumber = getBlockNumber(blockTag);
const multicall = getMulticall(blockNumber, this.network.chainId, provider);
const multicall = getMulticall(blockNumber, Number(network.chainId), provider);

if (!to || !data || multicall == null || multicallAddresses.has(to.toLowerCase()))
return _perform(method, params);
if (!to || !data || multicall == null || multicallAddresses.has(to.toString().toLowerCase()))
return _perform(req);

this._debouncedPerformMulticall();

return new Promise<string>((resolve, reject) => {
return new Promise<R>((resolve, reject) => {
queuedCalls.push({
to,
data,
Expand Down
Loading

0 comments on commit 6680f52

Please sign in to comment.