Cookbook > Subgraph Best Practice 4: Avoid eth_calls

Subgraph Best Practice 4 - Improve Indexing Speed by Avoiding eth_calls

Reading time: 4 min

TLDR

Link to this section

eth_calls are calls that can be made from a subgraph to an Ethereum node. These calls take a significant amount of time to return data, slowing down indexing. If possible, design smart contracts to emit all the data you need so you don’t need to use eth_calls.

Why Avoiding eth_calls Is a Best Practice

Link to this section

Subgraphs are optimized to index event data emitted from smart contracts. A subgraph can also index the data coming from an eth_call, however, this can significantly slow down subgraph indexing as eth_calls require making external calls to smart contracts. The responsiveness of these calls relies not on the subgraph but on the connectivity and responsiveness of the Ethereum node being queried. By minimizing or eliminating eth_calls in our subgraphs, we can significantly improve our indexing speed.

What Does an eth_call Look Like?

Link to this section

eth_calls are often necessary when the data required for a subgraph is not available through emitted events. For example, consider a scenario where a subgraph needs to identify whether ERC20 tokens are part of a specific pool, but the contract only emits a basic Transfer event and does not emit an event that contains the data that we need:

event Transfer(address indexed from, address indexed to, uint256 value);

Suppose the tokens' pool membership is determined by a state variable named getPoolInfo. In this case, we would need to use an eth_call to query this data:

import { Address } from '@graphprotocol/graph-ts'
import { ERC20, Transfer } from '../generated/ERC20/ERC20'
import { TokenTransaction } from '../generated/schema'
export function handleTransfer(event: Transfer): void {
let transaction = new TokenTransaction(event.transaction.hash.toHex())
// Bind the ERC20 contract instance to the given address:
let instance = ERC20.bind(event.address)
// Retrieve pool information via eth_call
let poolInfo = instance.getPoolInfo(event.params.to)
transaction.pool = poolInfo.toHexString()
transaction.from = event.params.from.toHexString()
transaction.to = event.params.to.toHexString()
transaction.value = event.params.value
transaction.save()
}

This is functional, however is not ideal as it slows down our subgraph’s indexing.

How to Eliminate eth_calls

Link to this section

Ideally, the smart contract should be updated to emit all necessary data within events. For instance, modifying the smart contract to include pool information in the event could eliminate the need for eth_calls:

event TransferWithPool(address indexed from, address indexed to, uint256 value, bytes32 indexed poolInfo);

With this update, the subgraph can directly index the required data without external calls:

import { Address } from '@graphprotocol/graph-ts'
import { ERC20, TransferWithPool } from '../generated/ERC20/ERC20'
import { TokenTransaction } from '../generated/schema'
export function handleTransferWithPool(event: TransferWithPool): void {
let transaction = new TokenTransaction(event.transaction.hash.toHex())
transaction.pool = event.params.poolInfo.toHexString()
transaction.from = event.params.from.toHexString()
transaction.to = event.params.to.toHexString()
transaction.value = event.params.value
transaction.save()
}

This is much more performant as it has eliminated the need for eth_calls.

How to Optimize eth_calls

Link to this section

If modifying the smart contract is not possible and eth_calls are required, read “Improve Subgraph Indexing Performance Easily: Reduce eth_calls” by Simon Emanuel Schmid to learn various strategies on how to optimize eth_calls.

Reducing the Runtime Overhead of eth_calls

Link to this section

For the eth_calls that can not be eliminated, the runtime overhead they introduce can be minimized by declaring them in the manifest. When graph-node processes a block it performs all declared eth_calls in parallel before handlers are run. Calls that are not declared are executed sequentially when handlers run. The runtime improvement comes from performing calls in parallel rather than sequentially - that helps reduce the total time spent in calls but does not eliminate it completely.

Currently, eth_calls can only be declared for event handlers. In the manifest, write

event: TransferWithPool(address indexed, address indexed, uint256, bytes32 indexed)
handler: handleTransferWithPool
calls:
ERC20.poolInfo: ERC20[event.address].getPoolInfo(event.params.to)

The portion highlighted in yellow is the call declaration. The part before the colon is simply a text label that is only used for error messages. The part after the colon has the form Contract[address].function(params). Permissible values for address and params are event.address and event.params.<name>.

The handler itself accesses the result of this eth_call exactly as in the previous section by binding to the contract and making the call. graph-node caches the results of declared eth_calls in memory and the call from the handler will retrieve the result from this in memory cache instead of making an actual RPC call.

Note: Declared eth_calls can only be made in subgraphs with specVersion >= 1.2.0.

We can significantly improve indexing performance by minimizing or eliminating eth_calls in our subgraphs.

Edit page

Previous
Subgraph Best Practice 3: Using Immutable Entities and Bytes as IDs
Next
How to Secure API Keys Using Next.js Server Components
Edit page