Skip to main content

Batch-request and multicall smart contracts

Overview

While interacting with Ronin, you might want to speed up your process or improve semantic consistency. A way to do that is by submitting a single HTTP request with several sub-requests inside the JSON-RPC body.

Submitting several sub-requests in one has the following advantages:

  • Improved request and response latency.
  • Improved consistency. For example, when making several requests with the “latest“ BlockNumber, you expect the "latest" block number to be the same for all requests. This mechanism gives you a better chance of reaching that level, but not 100%.

This tutorial shows to make a batch JSON-RPC request to Ronin. It also describes how to wrap several smart contract calls in a single request and send it to a specific contract that performs multiple call resolution. The task used throughout the tutorial is, “I want to get the WETH, AXS, USDC, SLP balances of an address with just one request.“

Prerequisites

  • Node.js or your preferred web3 client written in another language.
  • npm or yarn for package installation. This tutorial uses npm.
  • A Ronin Wallet address.
  • Basic knowledge of HTTP.
  • Basic knowledge of JavaScript.
  • Basic knowledge of JSON-RPC. For more information, see the JSON-RPC specification.

Environment

Throughout this tutorial, you use the public RPC endpoint for the Saigon testnet: https://saigon-testnet.roninchain.com/rpc.

note

We recommend using the public endpoint only for testing. For production, make sure to use the developer portal's endpoint https://api-gateway.skymavis.com/rpc/testnet.

Step 1. Create a project

  1. Create a new directory for your project and open the terminal in that directory:

    cd ~/
    mkdir ronin_rpc_contract_sample
    cd ronin_rpc_contract_sample
  2. Initialize a new Node.js project:

    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"
    }
  3. Install the ethers.js library:

    npm install ethers@5.7.2

    This tutorial uses ethers.js version 5.7.2.

  4. Install the axios library:

    npm install axios

Step 2. Use a JSON-RPC batch request

About batch requests

A batch request is a single HTTP request that contains many nested JSON RPC methods. The client can send several request objects in an array and receive a corresponding array of response objects from the server.

The server processes all requests of this batch RPC call concurrently, in any order. The response objects can be in any order as well. The client should match the context of the request objects to the response objects based on the ID of each object.

For more information, see the JSON-RPC specification.

Example request:

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"
}
]

Query balances

At this step, you'll create a script to get balance (ERC20 token: WETH, AXS, USDC & SLP) of the address 0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3.

  1. Create a file called batch_request.js and paste the following code:

    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)}`);
    });
    })
  2. Execute the code by running the following command:

    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.

  1. These lines import the ethers and axios libraries for interacting with Ethereum and making HTTP requests, respectively.

    const {ethers} = require('ethers');
    const axios = require('axios');
  2. This array defines the contract's ABI. The ABI describes the structure of the contract, including its 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"
    }];
  3. The following line creates an instance of ethers.utils.Interface using the contract's ABI. This interface provides utility functions for encoding and decoding contract function calls.

    const ERC20ContractInterface = new ethers.utils.Interface(contractABI);
  4. These lines define an array contractAddresses that contains the addresses of the contracts we want to interact with on the Saigon testnet.

    const contractAddresses = [
    '0x29c6f8349a028e1bdfc68bfa08bdee7bc5d47e16', //WETH
    '0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d', //AXS
    '0x067FBFf8990c58Ab90BaE3c97241C5d736053F77', //USDC
    '0x82f5483623d636bc3deba8ae67e1751b6cf2bad2' //SLP
    ];
  5. This initializes an empty array callArray that will store the JSON-RPC call objects for each contract.

    const callArray = []
  6. 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.

    1. 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.
    2. 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)
    })
    })
  7. Finally, an HTTP POST request is made to the Saigon testnet RPC endpoint https://saigon-testnet.roninchain.com/rpc' using axios.post. The array callArray is sent as the request payload.

    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 are making a balanceOf call on each contract for a specific address 0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3. Make sure to adjust the address or modify the code as per your requirements.

Step 3. Use a multicall smart contract

About multicall

A multicall contract is a smart contract that takes function call objects as parameters and executes them together. You can use the multicall contract as a proxy for interacting with other smart contracts in a "batch" style. With multicall, a single request to the eth_call endpoint returns the results of many contract function calls.

Multicall benefits include the following:

  • Fewer JSON-RPC requests to send. This reduces the cost of RPC services as well as the number of round trips between the client and the node.
  • All returned values are guaranteed to be from the same block.
  • The block number or timestamp can be returned in the response, helping you detect stale data.

Query balances

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 file called multicall_contract.js and paste the following code:

    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)}`);
    });
    })
  2. Execute the code by running the following command:

    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.

  1. This line imports the ethers library, which provides utilities and classes for interacting with Ethereum.

    const { ethers } = require('ethers');
  2. Here, 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');
  3. These lines define the ABI and interface of an ERC20 contract. 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);
  4. These lines define the ABI and interface of a multicall contract. Similar to step 3, 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);
  5. This line defines an array of contract addresses, which represent the ERC20 contracts for which you want to retrieve the balance.

    const contractAddresses = [...]; // Array of contract addresses
  6. This code creates a call array to store the contract addresses and encoded function data for the balanceOf function. The forEach loop iterates over the contract addresses, and for each address, the function data is encoded using the ERC20ContractInterface.encodeFunctionData method and added to the call array.

    const callArray = [];
    contractAddresses.forEach((contractAddress, index) => {
    callArray.push([
    contractAddress,
    ERC20ContractInterface.encodeFunctionData('balanceOf', ['0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3'])
    ]);
    });
  7. Finally, a multicall request is made using the provider.call method. The to field specifies the address of the multicall contract, and the data field contains the encoded function data for the aggregate function, which takes the call array as a parameter. The response is then processed to decode the returned data using the multicallInterface.decodeFunctionResult method. The results are retrieved from the decoded data and printed to the console.

    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 helpful?
Happy React is loading...