Chain Whispering: Making RPC Calls

🕵️ Learn how to directly query smart contracts

Jul 23, 2022    m. Sep 14, 2024    #blockchain  

Introduction

Ever wondered how crypto price watchers like CoinMarketCap and crypto portfolio apps like Debank get their data?

It may seem like rocket science at first glance, but when distilled down to its essence, it primarily involves making remote procedure calls (RPCs) to blockchains to extract real-time data and subsequently caching or storing this data.

RPCs are essentially API standards to call functions in a program remotely from different IP address spaces - in blockchain speak, the ‘program’ would be smart contracts.

In this article, we will explore how to make these RPC calls on Ethereum Virtual Machine(EVM)-compatible blockchains through concrete examples, and demonstrate how easy it is to become a chain whisperer.

Native Method

EVM-based blockchains like Ethereum and Polygon use a lightweight RPC protocol called JSON-RPC that uses JSON as its data format to transport payloads over HTTP.

To demonstrate from first principles, we will first make an RPC call natively, to try to find out:

What is the total supply of MANA tokens issued on the Polygon (MATIC) blockchain?

Step 1: Encode Function Names

In order to call functions from smart contracts, we need to convert the function names from high-level strings to machine-readable bytecode. EVM requires function calls to be formatted in a specific way as outline in the official docs :

MANA tokens are ERC-20 tokens, and so by referencing the ERC-20 standards for function names, or alternatively the ERC-20 Application Binary Interface (ABI) , we need the following functions to find the total supply of MANA tokens:

Luckily, our functions do not take any inputs, so no worry about appending.

The code

We can use Python and its Pycryptodome crypto library to encode the above function names from string to hex, with the following code:

# perform `pip install pycryptodome` if library hasn't been installed

from Crypto.Hash import keccak

'''
Encode totalSupply()
'''
k = keccak.new(digest_bits=256)
k.update(b'totalSupply()')

TOTAL_SUPPLY_HEX = '0x' + k.hexdigest()[0:8]

'''
Encode decimals()
'''
t = keccak.new(digest_bits=256)
t.update(b'decimals()')

DECIMALS_HEX = '0x' + t.hexdigest()[0:8]


print('TOTAL_SUPPLY_HEX= ' + TOTAL_SUPPLY_HEX)
print('DECIMALS_HEX= ' + DECIMALS_HEX)

The printed results should show:

TOTAL_SUPPLY_HEX= 0x18160ddd

DECIMALS_HEX= 0x313ce567

Step 2: Making RPC request to node endpoint

Now we construct our HTTP request to the public RPC node on Polygon (https://polygon-rpc.com/) .

We want to format a curl command in Python, using the ‘Requests’ library for helpers.

The JSON RPC command that we’ll be using is the eth_call (refer here ), and it looks like this:

curl https://polygon-rpc.com/ -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4","data":"0x18160ddd"},"latest"],"id":1}'

The raw undecoded returned result is in JSON format, like such:

{"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000003223189d56d69e16367db"}

Step 2 Alternative: Using HTTP request helper library

Alternatively, we can use the Requests library in Python to simplify this process:

# perform `pip install requests` if library hasn't been installed

import requests

HEADERS = {
    # Already added when you pass json=
    # 'Content-Type': 'application/json',
}

TOTAL_SUPPLY_DATA = {
    'jsonrpc': '2.0',
    'method': 'eth_call',
    'params': [
        {
            'to': '0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4',
            'data': 0x18160ddd,  # totalSupply() call function
        },
        'latest',
    ],
    'id': 1,
}


DECIMALS_DATA = {
    'jsonrpc': '2.0',
    'method': 'eth_call',
    'params': [
        {
            'to': '0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4',
            'data': 0x313ce567 # decimals() call function
        },
        'latest',
    ],
    'id': 1,
}

We can now make the RPC requests.

Most nodes don’t support batch requests, hence, we will be doing 2 separate requests instead.

There is an option to make multicalls at once by sending a txn to Multicall contracts like Uniswap’s multicall contract , which takes an array of Call inputs. Reference found here .

'''
totalSupply() request
'''

supply_request = requests.post('https://polygon-rpc.com/',
                               headers=HEADERS, json=TOTAL_SUPPLY_DATA)

supply_response = supply_request.json()

supply_result = int(supply_response['result'], 16)



'''
decimals() request
'''
decimals_request = requests.post('https://polygon-rpc.com/',
                                 headers=HEADERS, json=DECIMALS_DATA)

decimals_response = decimals_request.json()

decimals_result = int(decimals_response['result'], 16)


print('supply_result= ' + str(supply_result))
print('decimals_result= ' + str(decimals_result))

The printed result should show:

supply_result= 3395497579944206839225728

decimals_result= 18

We can then calculate the total supply of MANA tokens on Polygon:

print('Total MANA supply on Polygon: ', supply_result / 10**decimals_result)

At time of writing, the printed result shows:

Total MANA supply on Polygon:  3395497.579944207

Library Method

Now, to make things easier, let’s use a Web3 helper library like Web3.js and Ethers.js.

To demonstrate, we will be using Web3.js to try to find out the following:

How many swap events on Quickswap (a DEX on Polygon) were emitted for the block #26444465? Given that a swap event is identified with the hash ID 0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822 .

As implied by the library names, we will be using Typescript to write our RPC call script.

Step 1: Environment setup

Our initial step will be to import the Web3.js library and set our RPC node endpoint:

// Import web3js library, set Polygon RPC as provider

const Web3 = require("web3");
const web3 = new Web3(
  new Web3.providers.HttpProvider("https://polygon-rpc.com/"),
);

Step 2: Extract relevant section of Application Binary Interface (ABI)

In order to interact with the smart contract on a high-level, we need the Application Binary Interface (ABI) which outlines all possible function calls & its bytecode equivalent to allow easy encoding/decoding.

For this particular task, we will only need the section that outlines the details of the ‘Swap’ event:

/**
 * QuickSwap contract based on UniswapV2 - events detailed in interface contract IUniswapV2Pair.sol
 * Swap event ABI from https://unpkg.com/@uniswap/[email protected]/build/IUniswapV2Pair.json
 */

const swapABI = [
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: "address",
        name: "sender",
        type: "address",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount0In",
        type: "uint256",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount1In",
        type: "uint256",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount0Out",
        type: "uint256",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount1Out",
        type: "uint256",
      },
      {
        indexed: true,
        internalType: "address",
        name: "to",
        type: "address",
      },
    ],
    name: "Swap",
    type: "event",
  },
];

Step 3: Set up RPC call functions

Now, with the ABI, we can setup our functions to easily make the RPC call to the public node endpoint.

We are given the blockNumber and eventID variables:

// Get Swap event from block #26444465

const blockNumber = web3.utils.toHex(26444465);

const eventID: string =
  '0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822';

We can then write our ‘getSwapLog’ function to get the Swap event logs:

/**
 * @dev Get event logs
 * @param fromBlock - Number|String : no. of earliest block, default "latest"
 * @param toBlock - Number|String : no. of latest block, default "latest"
 * @param topics - Array : array of values in log entries, in this case the given hash ID for the event 'Swap'
 * @returns Promise returns Array - array of log objects
 */

async function getSwapLog() {
  const rawResult = await web3.eth.getPastLogs({
    fromBlock: blockNumber,
    toBlock: blockNumber,
    topics: [eventID],
  });

  return rawResult;
}

Next, we need to define a function to decode the event logs response:

/**
 * @dev Decode event logs
 * @param inputs- Object : JSON interface inputs array, from the contract ABI
 * @param hexString - String : nABI byte code in 'data' field of a log
 * @param topics - Array : array with index parameter topics of log, without topic[0] for non-aonymous event
 * @returns Object - decoded log event
 */

interface LogProps {
  data: string;
  topics: Array<string>;
}

function decodeLog(log: LogProps) {
  const decodedResult = web3.eth.abi.decodeLog(
    swapABI[0].inputs,
    log.data,
    log.topics.slice(1) // method requires excluding topic[0]
  );

  return decodedResult;
}

Step 4: Execute script

We can now execute functions with a loop like this:

/**
 * getSwapLog & decodeLog execution
 */

getSwapLog().then((logs) => {
  logs.forEach((log: any) => {
    console.log('Raw Log: ', log);
    console.log('Decoded Log: ', decodeLog(log));
  });
});

To answer the question, there is only 1 swap txn found in the block #26444465, under the txn hash 0x3483fd0fa5d38905e28245ca9ca87b0ab331a104c509e3d239be8f1e5337c01b - it is a swap from 0.004487488303473224 WETH to 8.713302115784607036 MATIC

The raw RPC call response is:

[
  {
    address: "0xadbF1854e5883eB8aa7BAf50705338739e558E5b",
    topics: [
      "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822",
      "0x000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff",
      "0x000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff",
    ],
    data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff158ca43224800000000000000000000000000000000000000000000000078ebde1bf800453c0000000000000000000000000000000000000000000000000000000000000000",
    blockNumber: 26444465,
    transactionHash:
      "0x3483fd0fa5d38905e28245ca9ca87b0ab331a104c509e3d239be8f1e5337c01b",
    transactionIndex: 29,
    blockHash:
      "0x8f5210b3052133904b5596a1345dc361e832eed56b181226c459eecb51113336",
    logIndex: 91,
    removed: false,
    id: "log_6702c1ca",
  },
];

The decoded event log is:

Result {
    '0': '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff',
    '1': '0',
    '2': '4487488303473224',
    '3': '8713302115784607036',
    '4': '0',
    '5': '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff',
    __length__: 6,
    sender: '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff',
    amount0In: '0',
    amount1In: '4487488303473224',
    amount0Out: '8713302115784607036',
    amount1Out: '0',
    to: '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff'
}

This shows a Swap event of 0.004487488303473224 WETH to 8.713302115784607036 MATIC.

Block Explorers Method

If you are in need of making a quick and easy query, using a block explorer like Etherscan is the way to go.

To demonstrate this, we try to answer the following question:

What is the metadata for NFT serial #3000 for the Bored Ape Yacht Club (BAYC) NFT collection?

Using Etherscan’s Read Contract feature , we can make the function call tokenURI(uint256 tokenId) to obtain the token Uniform Resource Indicator which stores the JSON metadata.

When we call tokenURI(3000) we get the following result:

Q1 Metadata Result

Metadata stored on ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/3000

The results tell us that the metadata is stored on the InterPlanetary File System (IPFS) , a peer-to-peer network protocol for storing and sharing data in a distributed file system that is content-addressed , unlike the more established BitTorrent protocol.

To access the link ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/3000, we need to have an IPFS client installed in our browser, or alternatively, we can use public gateways instead:

https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/3000

We get the following JSON response:

{
  "image": "ipfs://QmNrSwj6BYAPVDQ39kZF2nu4sRFD97g9983WBZe1XDNFWL",
  "attributes": [
    {
      "trait_type": "Fur",
      "value": "Tan"
    },
    {
      "trait_type": "Mouth",
      "value": "Grin"
    },
    {
      "trait_type": "Clothes",
      "value": "Black T"
    },
    {
      "trait_type": "Earring",
      "value": "Silver Hoop"
    },
    {
      "trait_type": "Eyes",
      "value": "3d"
    },
    {
      "trait_type": "Hat",
      "value": "Fisherman's Hat"
    },
    {
      "trait_type": "Background",
      "value": "Blue"
    }
  ]
}

Conclusion

Contrary to what many believe, querying data directly on the blockchain doesn’t involve rocket science, but primarily RPC calls. In this article, we have covered step-by-step 3 approaches to making RPC calls to node endpoints: the native method via HTTP, using a helper library like Web3.js, and using a block explorer for quick queries.

With that, you can now proudly call yourself a Chain Whisperer!



Next: Liquidity Pools 101: The Risks of Providing Liquidity