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 ERC20 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 ERC20 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 ERC20'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 ERC20 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 ERC20 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 ERC20 contract are defined. The contractABI variable holds the ABI of an ERC20 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 ERC20 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

Was this page helpful?
Happy React is loading...