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
- Knowledge of HTTP and JavaScript.
- Understanding of JSON-RPC; see the JSON-RPC specification for details.
- Node.js installed on your machine.
- Package manager like npm or yarn.
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
-
Initialize your project:
mkdir ronin_rpc_contract_sample
cd ronin_rpc_contract_sample
npm init -yYou 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"
} -
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
-
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.
-
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 andaxios
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 arraycallArray
. -
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 identifierid
. -
The
encodeFunctionData
method ofERC20ContractInterface
is used to encode the function name (balanceOf
) and its arguments (the address0xf6fd5fca4bd769ba495b29b98dba5f2ecf4ceed3
) 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.
-
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.
-
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. TheERC20ContractInterface
is created using theethers.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 themulticallInterface
is created using theethers.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)}`,
);
});
});