Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: eth_call multicall aggregation #387

Merged
merged 5 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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