DocumentationSubstreamsBasicsRPC Calls

RPC Calls

Sometimes, the data you need isn’t available from the events you are tracking in your Substreams. This data can be fetched via RPC calls; however, it’s important to minimize the use of RPC calls because they can slow down your Substreams processing.

The code generated using substreams init provides Rust bindings that can be used to access public state variables.

Let’s extend our Uniswap map_pools_created module to fetch details for token0 and token1.

Single RPC Calls

Our Substreams package is currently just tracking the UniswapFactoryV3 contract and all subsequent Pool contracts. To access token information related to pools that have been deployed, we will need to get these from the ERC20 contracts associated with the deployed pools. To do this, we should add the ERC20 ABI to our codebase’s abi directory and update the build.rs script to generate Rust bindings for ERC20 contracts. This exposes functions for fetching token names, symbols, and decimals.

For individual calls to a contract, you can use the call method:

let name = erc20_contract::functions::Name {}
    .call(event.token0.to_vec())
    .unwrap();

Batch RPC Calls

If multiple calls are needed, batching them reduces latency by sending all requests in a single batch.

let batch = rpc::RpcBatch::new();
 
let responses = batch
    .add(erc20_contract::functions::Name {}, event.token0.clone())
    .add(erc20_contract::functions::Symbol {}, event.token0.clone())
    .add(erc20_contract::functions::Decimals {}, event.token0.clone())
    .execute()
    .unwrap()
    .responses;
 
let name = substreams_ethereum::rpc::RpcBatch::decode::<_, erc20_contract::functions::Name>(&responses[0])
    .unwrap_or_else(|| {
        substreams::log::debug!("failed to get name");
        "".to_string()
    });
 
let symbol = substreams_ethereum::rpc::RpcBatch::decode::<_, erc20_contract::functions::Symbol>(&responses[1])
    .unwrap_or_else(|| {
        substreams::log::debug!("failed to get symbol");
        "".to_string()
    });
 
let decimals = substreams_ethereum::rpc::RpcBatch::decode::<_, erc20_contract::functions::Decimals>(&responses[2])
    .unwrap_or_else(|| {
        substreams::log::debug!("failed to get decimals");
        "".to_string()
    });

Error Handling

When making RPC calls or dealing with any Result or Option types in our Substreams, it’s crucial to handle potential errors gracefully. Using unwrap_or_else helps ensure that your Substreams can continue processing even if some RPC calls fail. Adding logs for errors can aid the debugging process.

Although we have utilized unwrap_or_else in the above code snippet, we can also safely decode the responses using other methods. Another method would be utilizing Rust pattern matching, which allows you to destructure and handle different cases in a clean and expressive way.

// Batching RPC calls
let batch = rpc::RpcBatch::new();
 
let responses = batch
    .add(erc20_contract::functions::Name {}, event.token0.clone())
    .add(erc20_contract::functions::Symbol {}, event.token0.clone())
    .add(erc20_contract::functions::Decimals {}, event.token0.clone())
    .execute()
    .unwrap()
    .responses;
 
let name: String;
match substreams_ethereum::rpc::RpcBatch::decode::<
    _,
    erc20_contract::functions::Name,
>(&responses[0])
{
    Some(decoded_name) => {
        name = decoded_name;
        substreams::log::debug!("decoded name: {}", name);
    }
    None => {
        substreams::log::debug!("failed to get name");
    }
};
 
let symbol: String;
match substreams_ethereum::rpc::RpcBatch::decode::<
    _,
    erc20_contract::functions::Symbol,
>(&responses[1])
{
    Some(decoded_symbol) => {
        symbol = decoded_symbol;
        substreams::log::debug!("decoded symbol: {}", symbol);
    }
    None => {
        substreams::log::debug!("failed to get symbol");
    }
};
 
let symbol: String;
match substreams_ethereum::rpc::RpcBatch::decode::<
    _,
    erc20_contract::functions::Symbol,
>(&responses[2])
{
    Some(decoded_symbol) => {
        symbol = decoded_symbol;
        substreams::log::debug!("decoded symbol: {}", symbol);
    }
    None => {
        substreams::log::debug!("failed to get symbol");
    }
};

Summary

RPC calls are a powerful tool for fetching data but should be used sparingly to avoid latency. Ideally, RPC calls should be used when there is no other means of getting the data required. Use batch calls when multiple requests are necessary, and always implement robust error handling to ensure your Substreams remain resilient.