Skip to main content

Batch-request and multicall smart contracts

Overview

Interacting with the Ronin blockchain can be optimized using batch JSON-RPC requests or multicall contracts. This approach is particularly effective for actions that require data consistency across multiple queries, such as fetching token balances from different contracts at a specific block height.

Advantages:

  • Reduced latency: By sending multiple requests at once, you reduce round trip times.
  • Data consistency: Ensures that all sub-requests relate to the same blockchain state, avoiding discrepancies in rapidly updating environments.

This tutorial will guide you through the process of making batch JSON-RPC requests and using multicall contracts to retrieve ERC-20 token balances (WETH, AXS, USDC, SLP) from a single address in one request.

Prerequisites

Environment setup

Use the Saigon testnet public RPC endpoint for testing purposes: https://saigon-testnet.roninchain.com/rpc. For production, switch to the secured endpoint https://api-gateway.skymavis.com/rpc/testnet

Step 1. Set up your project

  1. Initialize your project:

    mkdir ronin_rpc_contract_sample
    cd ronin_rpc_contract_sample
    npm init -y

    You should see output similar to this:

    Wrote to ***/ronin_rpc_contract_sample/package.json:

    {
    "name": "ronin_rpc_sample",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
    }
  2. Install necessary libraries:

    npm install ethers@5.7.2 axios

    ethers.js is a library that interacts with Ethereum and is compatible with Ronin. axios is used for making HTTP requests.

Step 2. Implement a batch JSON-RPC request

About batch requests

Batch requests allow you to execute multiple JSON-RPC calls in a single HTTP request.

Example of a batch request to fetch the block number four times:

curl https://saigon-testnet.roninchain.com/rpc \
-X POST \
-H "Content-Type: application/json" \
-d '[{"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": []},
{"jsonrpc": "2.0", "id": 2, "method": "eth_blockNumber", "params": []},
{"jsonrpc": "2.0", "id": 3, "method": "eth_blockNumber", "params": []},
{"jsonrpc": "2.0", "id": 4, "method": "eth_blockNumber", "params": []}]'

Example response:

[
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x119ab0e"
},
{
"jsonrpc": "2.0",
"id": 2,
"result": "0x119ab0e"
},
{
"jsonrpc": "2.0",
"id": 3,
"result": "0x119ab0e"
},
{
"jsonrpc": "2.0",
"id": 4,
"result": "0x119ab0e"
}
]

Retrieve multiple balances using batch requests

  1. Create a batch_request.js script to query ERC-20 balances:

    batch_request.js
    // Import library
    const { ethers } = require("ethers");
    const axios = require("axios");

    // Define your contract ABI and addresses
    const contractABI = [
    {
    constant: true,
    inputs: [
    {
    name: "_owner",
    type: "address",
    },
    ],
    name: "balanceOf",
    outputs: [
    {
    name: "balance",
    type: "uint256",
    },
    ],
    payable: false,
    stateMutability: "view",
    type: "function",
    },
    ];

    // Define contract interface
    const ERC20ContractInterface = new ethers.utils.Interface(contractABI);

    // Array of contract addresses on Saigon testnet
    const contractAddresses = [
    "0x29c6f8349a028e1bdfc68bfa08bdee7bc5d47e16", //WETH
    "0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d", //AXS
    "0x067FBFf8990c58Ab90BaE3c97241C5d736053F77", //USDC
    "0x82f5483623d636bc3deba8ae67e1751b6cf2bad2", //SLP
    ];

    const callArray = [];
    contractAddresses.forEach((contractAddress, index) => {
    callArray.push({
    jsonrpc: "2.0",
    method: "eth_call",
    params: [
    {
    to: contractAddress,
    data: ERC20ContractInterface.encodeFunctionData("balanceOf", [
    "0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3",
    ]),
    },
    "latest",
    ],
    id: Math.floor(Math.random() * 100000),
    });
    });

    axios
    .post("https://saigon-testnet.roninchain.com/rpc", callArray)
    .then((response) => {
    const results = response.data;
    results.forEach((result, index) => {
    console.log(
    `Balance of ${contractAddresses[index]}: ${BigInt(result.result)}`,
    );
    });
    });
    • This script constructs a batch request to fetch balances of WETH, AXS, USDC, and SLP for a specified address.
    • Responses are matched with requests using the id field.
  2. Execute your script:

    node batch_request.js

    You should see output similar to this:

    Balance of 0x29c6f8349a028e1bdfc68bfa08bdee7bc5d47e16: 11201224440355637251309597
    Balance of 0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d: 42316291079920745148389
    Balance of 0x067FBFf8990c58Ab90BaE3c97241C5d736053F77: 714649238541
    Balance of 0x82f5483623d636bc3deba8ae67e1751b6cf2bad2: 109999995842674717

Code explanation

Let's walk through the contents of batch_request.js.

  • The script starts by importing ethers for blockchain interaction and axios for HTTP requests.

    const { ethers } = require("ethers");
    const axios = require("axios");
  • The contract ABI for ERC-20's balanceOf function is defined to interact with the blockchain. The ABI is an array of objects that describe the contract's functions, inputs, outputs, and other properties.

    const contractABI = [
    {
    constant: true,
    inputs: [
    {
    name: "_owner",
    type: "address",
    },
    ],
    name: "balanceOf",
    outputs: [
    {
    name: "balance",
    type: "uint256",
    },
    ],
    payable: false,
    stateMutability: "view",
    type: "function",
    },
    ];
  • An ethers interface is created for encoding and decoding calls to the smart contract.

    const ERC20ContractInterface = new ethers.utils.Interface(contractABI);
  • For each token contract, a JSON-RPC call object is crafted to query the balance and added to the batch request.

    const contractAddresses = [
    "0x29c6f8349a028e1bdfc68bfa08bdee7bc5d47e16", //WETH
    "0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d", //AXS
    "0x067FBFf8990c58Ab90BaE3c97241C5d736053F77", //USDC
    "0x82f5483623d636bc3deba8ae67e1751b6cf2bad2", //SLP
    ];
  • An empty array is initiated that will store the JSON-RPC call objects for each contract.

    const callArray = [];
  • A loop iterates over each contract address in contractAddresses. For each address, a JSON-RPC call object is created and added to the array callArray.

  • The JSON-RPC call object follows the JSON-RPC 2.0 specification. It specifies the method eth_call, the parameters for the call, including the contract address and the encoded function data, and an identifier id.

  • The encodeFunctionData method of ERC20ContractInterface is used to encode the function name (balanceOf) and its arguments (the address 0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3) into the contract's function call data.

    contractAddresses.forEach((contractAddress, index) => {
    callArray.push({
    jsonrpc: "2.0",
    method: "eth_call",
    params: [
    {
    to: contractAddress,
    data: ERC20ContractInterface.encodeFunctionData("balanceOf", [
    "0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3",
    ]),
    },
    "latest",
    ],
    id: Math.floor(Math.random() * 100000),
    });
    });
  • The batch of requests is sent using axios, and responses are handled to display the balances.

    axios
    .post("https://saigon-testnet.roninchain.com/rpc", callArray)
    .then((response) => {
    const results = response.data;
    results.forEach((result, index) => {
    console.log(
    `Balance of ${contractAddresses[index]}: ${BigInt(result.result)}`,
    );
    });
    });

Note: The code assumes you make a balanceOf call on each contract for the address 0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3. Make sure to adjust the address or edit the code example accordingly.

Step 3. Use a multicall smart contract

About multicall contracts

Multicall contracts streamline interactions by allowing a single contract call to execute multiple actions.

Benefits:

  • Efficiency: reduces the number of RPC calls.
  • Atomicity: ensures all calls are executed at the same block state.
  • Consistency: optionally returns the block number to verify the timeliness of data.

Retrieve multiple balances using multicall contracts

In this tutorial, you make requests to a multicall contract that's already deployed on the Saigon testnet at the address 0x31c9ef8a631e2489e69833df3b2cb4bf0dc413bc. You can view it in the Ronin app.

  1. Create a multicall_contract.js script to query ERC-20 balances:

    multicall_contract.js
    // Import library
    const { ethers } = require("ethers");

    // Initialize the provider
    const provider = new ethers.providers.JsonRpcProvider(
    "https://saigon-testnet.roninchain.com/rpc",
    );

    // Define your ERC-20 contract ABI and interface
    const contractABI = [
    {
    constant: true,
    inputs: [
    {
    name: "_owner",
    type: "address",
    },
    ],
    name: "balanceOf",
    outputs: [
    {
    name: "balance",
    type: "uint256",
    },
    ],
    payable: false,
    stateMutability: "view",
    type: "function",
    },
    ];
    const ERC20ContractInterface = new ethers.utils.Interface(contractABI);

    const multicallContractABI = [
    {
    constant: false,
    inputs: [
    {
    internal_type: "",
    name: "_calls",
    type: "tuple[]",
    components: [
    {
    internal_type: "",
    name: "target",
    type: "address",
    },
    {
    internal_type: "",
    name: "callData",
    type: "bytes",
    },
    ],
    indexed: false,
    },
    ],
    name: "aggregate",
    outputs: [
    {
    internal_type: "",
    name: "_blockNumber",
    type: "uint256",
    indexed: false,
    },
    {
    internal_type: "",
    name: "_returnData",
    type: "bytes[]",
    indexed: false,
    },
    ],
    payable: false,
    stateMutability: "nonpayable",
    type: "function",
    anonymous: false,
    },
    ];
    const multicallInterface = new ethers.utils.Interface(multicallContractABI);

    // Array of contract addresses
    const contractAddresses = [
    "0x29c6f8349a028e1bdfc68bfa08bdee7bc5d47e16", //WETH
    "0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d", //AXS
    "0x067FBFf8990c58Ab90BaE3c97241C5d736053F77", //USDC
    "0x82f5483623d636bc3deba8ae67e1751b6cf2bad2", //SLP
    ];

    const callArray = [];
    contractAddresses.forEach((contractAddress, index) => {
    callArray.push([
    contractAddress,
    ERC20ContractInterface.encodeFunctionData("balanceOf", [
    "0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3",
    ]),
    ]);
    });

    provider
    .call({
    to: "0x31c9ef8a631e2489e69833df3b2cb4bf0dc413bc",
    data: multicallInterface.encodeFunctionData("aggregate", [callArray]),
    })
    .then((response) => {
    const callResult = multicallInterface.decodeFunctionResult(
    "aggregate",
    response,
    );
    const results = callResult._returnData;
    results.forEach((result, index) => {
    console.log(
    `Balance of ${contractAddresses[index]}: ${BigInt(result)}`,
    );
    });
    });

    This script uses a deployed multicall contract to execute multiple balance checks in one transaction.

  2. Execute your script:

    node multicall_contract.js

    You should see output similar to this:

    Balance of 0x29c6f8349a028e1bdfc68bfa08bdee7bc5d47e16: 11201224440355637251309597
    Balance of 0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d: 42316291079920745148389
    Balance of 0x067FBFf8990c58Ab90BaE3c97241C5d736053F77: 714649238541
    Balance of 0x82f5483623d636bc3deba8ae67e1751b6cf2bad2: 109999995842674717

Code explanation

Let's walk through the contents of multicall_contract.js.

  • The script starts by importing ethers for blockchain interaction.

    const { ethers } = require("ethers");
  • A JSON-RPC provider is created using the URL of the Ronin node.

    const provider = new ethers.providers.JsonRpcProvider(
    "https://saigon-testnet.roninchain.com/rpc",
    );
  • The ABI and interface of an ERC-20 contract are defined. The contractABI variable holds the ABI of an ERC-20 contract. The ERC20ContractInterface is created using the ethers.utils.Interface class and the contract's ABI.

    const contractABI = [...]; // ERC20 contract ABI
    const ERC20ContractInterface = new ethers.utils.Interface(contractABI);
  • The ABI and interface of a multicall contract are defined. The multicallContractABI variable holds the ABI of a multicall contract, and the multicallInterface is created using the ethers.utils.Interface class.

    const multicallContractABI = [...]; // Multicall contract ABI
    const multicallInterface = new ethers.utils.Interface(multicallContractABI);
  • An array of contract addresses is defined. The contracts represent the ERC-20 contracts for which you want to retrieve the balance.

    const contractAddresses = [...]; // Array of contract addresses
  • Calls are prepared for each token, specifying the contract and method to be invoked.

    const callArray = [];
    contractAddresses.forEach((contractAddress, index) => {
    callArray.push([
    contractAddress,
    ERC20ContractInterface.encodeFunctionData("balanceOf", [
    "0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3",
    ]),
    ]);
    });
  • A multicall is made, and the results are processed to print the balances.

    provider
    .call({
    to: "0x31c9ef8a631e2489e69833df3b2cb4bf0dc413bc",
    data: multicallInterface.encodeFunctionData("aggregate", [callArray]),
    })
    .then((response) => {
    const callResult = multicallInterface.decodeFunctionResult(
    "aggregate",
    response,
    );
    const results = callResult._returnData;
    results.forEach((result, index) => {
    console.log(
    `Balance of ${contractAddresses[index]}: ${BigInt(result)}`,
    );
    });
    });

See also

Ronin JSON-RPC API reference