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)}`);
});
})