Skip to content

Commit

Permalink
feat: eth_call multicall aggregation (#387)
Browse files Browse the repository at this point in the history
* feat: add eth_call multicall aggregation

* fix: types

* docs

* chore: changeset

* docs
  • Loading branch information
jxom authored Apr 19, 2023
1 parent 98437eb commit 230fcfd
Show file tree
Hide file tree
Showing 33 changed files with 1,169 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-ravens-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added support for `eth_call` batch aggregation via multicall `aggregate3`.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ ALCHEMY_ID=
VITE_ANVIL_FORK_URL=
VITE_ANVIL_BLOCK_TIME=1
VITE_ANVIL_BLOCK_NUMBER=16280770
VITE_NETWORK_TRANSPORT_MODE=http
VITE_NETWORK_TRANSPORT_MODE=http
VITE_BATCH_MULTICALL=false
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ jobs:
shard: [1, 2, 3]
total-shards: [3]
transport-mode: ['http', 'webSocket']
include:
- batch-multicall: 'false'
- batch-multicall: 'true'
transport-mode: 'http'
steps:
- uses: actions/checkout@v3
- name: Setup
Expand All @@ -100,6 +104,7 @@ jobs:
VITE_ANVIL_BLOCK_TIME: ${{ vars.VITE_ANVIL_BLOCK_TIME }}
VITE_ANVIL_FORK_URL: ${{ vars.VITE_ANVIL_FORK_URL }}
VITE_NETWORK_TRANSPORT_MODE: ${{ matrix.transport-mode }}
VITE_BATCH_MULTICALL: ${{ matrix.batch-multicall }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
107 changes: 107 additions & 0 deletions site/docs/clients/public.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,54 @@ Then you can consume [Public Actions](/docs/actions/public/introduction):
const blockNumber = await client.getBlockNumber() // [!code focus:10]
```

## Optimization

The Public Client also supports [`eth_call` Aggregation](#multicall) and <span class="opacity-50 font-medium">JSON-RPC Batching (soon)</span> for improved performance.

### `eth_call` Aggregation (via Multicall)

The Public Client supports the aggregation of `eth_call` requests into a single multicall (`aggregate3`) request.

This means for every Action that utilizes an `eth_call` request (ie. `readContract`), the Public Client will batch the requests (over a timed period) and send it to the RPC Provider in a single multicall request. This can dramatically improve network performance, and decrease the amount of [Compute Units (CU)](https://docs.alchemy.com/reference/compute-units) used by RPC Providers like Alchemy, Infura, etc.

The Public Client schedules the aggregation of `eth_call` requests over a given time period. By default, it executes the batch request at the end of the current [JavaScript message queue](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#queue) (a [zero delay](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#zero_delays)), however, consumers can specify a custom `wait` period (in ms).

You can enable `eth_call` aggregation by setting the `batch.multicall` flag to `true`:

```ts
const client = createPublicClient({
batch: {
multicall: true, // [!code focus]
},
chain: mainnet,
transport: http(),
})
```

> You can also [customize the `multicall` options](http://localhost:5173/docs/clients/public.html#batch-multicall-batchsize-optional).
Now, when you start to utilize `readContract` Actions, the Public Client will batch and send over those requests at the end of the message queue (or custom time period) in a single `eth_call` multicall request:

```ts
const contract = getContract({ address, abi })

// The below will send a single request to the RPC Provider.
const [name, totalSupply, symbol, tokenUri, balance] = await Promise.all([
contract.read.name(),
contract.read.totalSupply(),
contract.read.symbol(),
contract.read.tokenURI([420n]),
contract.read.balanceOf([address]),
])
```

> Read more on [Contract Instances](http://localhost:5173/docs/contract/getContract.html).

### JSON-RPC Batching

The Public Client will support [JSON-RPC Batching](https://www.jsonrpc.org/specification#batch). This is coming soon.

## Parameters

### transport
Expand Down Expand Up @@ -72,6 +120,65 @@ const client = createPublicClient({
})
```

### batch (optional)

Flags for batch settings.

### batch.multicall (optional)

- **Type:** `boolean | MulticallBatchOptions`
- **Default:** `false`

Toggle to enable `eth_call` multicall aggregation.

```ts
const client = createPublicClient({
batch: {
multicall: true, // [!code focus]
},
chain: mainnet,
transport: http(),
})
```

### batch.multicall.batchSize (optional)

- **Type:** `number`
- **Default:** `1_024`

The maximum size (in bytes) for each multicall (`aggregate3`) calldata chunk.

```ts
const client = createPublicClient({
batch: {
multicall: {
batchSize: 512, // [!code focus]
},
},
chain: mainnet,
transport: http(),
})
```

### batch.multicall.wait (optional)

- **Type:** `number`
- **Default:** `0` ([zero delay](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#zero_delays))

The maximum number of milliseconds to wait before sending a batch.

```ts
const client = createPublicClient({
batch: {
multicall: {
wait: 16, // [!code focus]
},
},
chain: mainnet,
transport: http(),
})
```

### key (optional)

- **Type:** `string`
Expand Down
6 changes: 6 additions & 0 deletions src/_test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,18 @@ const provider = {
}

export const httpClient = createPublicClient({
batch: {
multicall: process.env.VITE_BATCH_MULTICALL === 'true',
},
chain: anvilChain,
pollingInterval: 1_000,
transport: http(),
})

export const webSocketClient = createPublicClient({
batch: {
multicall: process.env.VITE_BATCH_MULTICALL === 'true',
},
chain: anvilChain,
pollingInterval: 1_000,
transport: webSocket(localWsUrl),
Expand Down
3 changes: 1 addition & 2 deletions src/actions/ens/getEnsAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ test('invalid universal resolver address', async () => {
universalResolverAddress: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The contract function \\"resolve\\" reverted with the following reason:
execution reverted
"The contract function \\"resolve\\" reverted.
Contract Call:
address: 0x0000000000000000000000000000000000000000
Expand Down
3 changes: 1 addition & 2 deletions src/actions/ens/getEnsName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ test('invalid universal resolver address', async () => {
universalResolverAddress: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The contract function \\"reverse\\" reverted with the following reason:
execution reverted
"The contract function \\"reverse\\" reverted.
Contract Call:
address: 0x0000000000000000000000000000000000000000
Expand Down
3 changes: 1 addition & 2 deletions src/actions/ens/getEnsResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ test('invalid universal resolver address', async () => {
universalResolverAddress: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The contract function \\"findResolver\\" reverted with the following reason:
execution reverted
"The contract function \\"findResolver\\" reverted.
Contract Call:
address: 0x0000000000000000000000000000000000000000
Expand Down
3 changes: 1 addition & 2 deletions src/actions/ens/getEnsText.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ test('invalid universal resolver address', async () => {
universalResolverAddress: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The contract function \\"resolve\\" reverted with the following reason:
execution reverted
"The contract function \\"resolve\\" reverted.
Contract Call:
address: 0x0000000000000000000000000000000000000000
Expand Down
6 changes: 2 additions & 4 deletions src/actions/getContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,7 @@ test('js reserved keywords/prototype methods as abi item names', async () => {
await expect(
contractNoIndexedEventArgs.read.constructor(),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The contract function \\"constructor\\" reverted with the following reason:
execution reverted
"The contract function \\"constructor\\" reverted.
Contract Call:
address: 0x0000000000000000000000000000000000000000
Expand All @@ -183,8 +182,7 @@ test('js reserved keywords/prototype methods as abi item names', async () => {
await expect(
contractNoIndexedEventArgs.read.function(['function']),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The contract function \\"function\\" reverted with the following reason:
execution reverted
"The contract function \\"function\\" reverted.
Contract Call:
address: 0x0000000000000000000000000000000000000000
Expand Down
Loading

1 comment on commit 230fcfd

@vercel
Copy link

@vercel vercel bot commented on 230fcfd Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viem-playground – ./playgrounds/browser

viem-playground.vercel.app
viem-playground-git-main-wagmi-dev.vercel.app
viem-playground-wagmi-dev.vercel.app

Please sign in to comment.