Querying full and archive EVM nodes with Python

Querying full vs archive nodes tutorial on Chainstack

Introduction

Most EVM-based blockchains usually can run two types of nodes: full and archive nodes.  

Chainstack supports many popular EVM-based networks, including EthereumPolygonBNB Smart ChainC-Chain of Avalanche.  

The main difference between a full node and an archive node is that an archive node stores the entire transactional history of the blockchain since its genesis. On the other hand, full nodes typically store the most recent states, typically up to the latest 128 blocks. Full nodes may serve older historical data but are inefficient for this task.  

A profound explanation of how a full node differs from an archive node is on EVM nodes: A dive into the full vs. archive mode.  

In this series, we will focus on retrieving historical data from the blockchain programmatically, switching between a full and archive node provider when necessary. The tutorial is two parts that demonstrate the process using two popular web3 languages: JavaScript and Python.

Initial setup

Let us dive deeper into how we can access the data stored in the network. Some essential functions include getting an address balance and storage at a given position, a contract bytecode, or even the whole transactions included on a given block. We can also query states from a given block for any custom smart contract of our choice. 

Today’s tutorial will query the blockchain utilizing the web3 and inquirer libraries for Python. 

First, set up a new Python project: 

mkdir full-archive-querys-py && cd full-archive-querys-py 
pip install web3 inquirer 

We added web3.py, which will be a crucial interface to interact with the blockchain. We also added the Inquirer Python package to build a console application. Our goal is that a user can select some desired query to perform between standard state functions or from a custom smart contract

We will call state functions from the SHIBA-INU ERC-20 token contract in this example. Initially, we will create three files: the main script, some utility functions, and the SHIBA-INU (or your custom contract) token contract ABI. 

touch main.py  
mkdir src && cd src  
touch utils.py abi.json 

Overall, our project structure should initially look like this: 

├── main.py 
└── src 
    ├── abi.json 
    └── utils.py 

Introducing common state functions

Let us begin setting up the queries for common state functions. We will implement a few popular ones, like fetching an address ETH balance or getting the transactions mined on a given block. A full and archive node is required to follow up on this tutorial. Get a free public node from Chainstack. Open the utils.py file and add the following code: 

# Imports
import json
from web3 import Web3
from pprint import pprint
# Init full and archive provider
full_node_provider = Web3(
    Web3.HTTPProvider(
        "https://nd-479-987-415.p2pify.com/271156373b36700f7576cf46e68b1262"
    )
)
archive_node_provider = Web3(
    Web3.HTTPProvider(
        "https://nd-072-228-848.p2pify.com/3f7a80739e2f6739cae0256a2660725b"
    )
)

def to_checksum_address(address):
    return full_node_provider.toChecksumAddress(address)

def to_hex(string):
    return full_node_provider.toHex(string)

# Returns the current block number of a network
def get_block_number():
    return full_node_provider.eth.block_number

# Returns the ETH balance of a given address at a given block
def get_eth_balance(address, block):
    print(
        "[QUERYING] Fetching ETH balance from address {} at block {}".format(
            address, block
        )
    )
    try:
        print("[QUERYING] Attempting with full Node")
        return full_node_provider.eth.get_balance(address, block)
    except Exception as e:
        if "missing trie node" in str(e):
            print("[OLD-BLOCK-QUERY] Switching to archive query")
            return archive_node_provider.eth.get_balance(address, block)
        else:
            print("exception: ", e)
            return None

# Returns the storage of an address at a given position and block
def get_storage_at(address, position, block):
    try:
        print(
            "[QUERYING] Fetching storage at address {} at position {} at block {}".format(
                address, position, block
            )
        )
        return full_node_provider.eth.get_storage_at(address, position, block)
    except Exception as e:
        if "missing trie node" in str(e):
            print("[OLD-BLOCK-QUERY] Switching to archive query")
            return archive_node_provider.eth.get_storage_at(address, position, block)
        else:
            return None

# Returns the code at a given address and block
def get_code(address, block):
    try:
        print(
            "[QUERYING] Fetching code at address {} at block {}".format(address, block)
        )
        return full_node_provider.eth.get_code(address, block)
    except Exception as e:
        if "missing trie node" in str(e):
            print("[OLD-BLOCK-QUERY] Switching to archive query")
            return archive_node_provider.eth.get_code(address, block)
        else:
            return None

# Returns the mined transactions in a given block
def get_block_transactions(block, full_transactions=False):
    try:
        print("[QUERYING] Fetching transactions from block {}".format(block))
        return full_node_provider.eth.get_block(block, full_transactions)
    except Exception as e:
        if "missing trie node" in str(e):
            print("[OLD-BLOCK-QUERY] Switching to archive query")
            return archive_node_provider.eth.get_block(block, full_transactions)
        else:
            return None

def print_eth_balance_of(address, block):
    eth_balance = get_eth_balance(address, block)
    print(
        "[BALANCE-RESULTS] Eth balance of address {} at block {}: {} $ETH".format(
            address, block, eth_balance
        )
    ) if eth_balance is not None else print("Invalid Query")

def print_storage_at(address, position, block):
    storage_at = full_node_provider.toHex(get_storage_at(address, position, block))
    print(
        "[STORAGE-AT-RESULTS] Storage at {} at position {} at block {}: {}".format(
            address, position, block, storage_at
        )
    ) if storage_at is not None else print("Invalid query")

def print_code_at(address, block):
    code_at = full_node_provider.toHex(get_code(address, block))
    print(
        "[CODE-AT-RESULTS] Code at address {} at block {}: {}".format(
            address, block, code_at
        )
    ) if code_at is not None else print("Invalid query")

def print_block_transactions(block, full):
    block_transactions = get_block_transactions(block, full)
    print(
        "[TRANSACTIONS] Transactions at block {}: {}".format(block, block_transactions)
    ) if block_transactions is not None else print("Invalid Query")

With these functions, you can query the network state of any block since the query execution will transition to the archive node provider if the block to fetch cannot be reached from a full node (usually 32-128 blocks past the latest block). 

Introducing custom smart contract queries

Even though these typical functions are handy sometimes, most cases require interactions with smart contracts to retrieve their states along the network timeline. For this reason, we will use a popular ERC-20 smart contract token as an example. We choose SHIBA INU, which has been living a long time on the Ethereum Mainnet, just for illustration purposes. Of course, any contract that exposes state calls will serve for testing. Just remember that the only interactions that we will perform are state calls. We will not change the network state.  

First, let us add the contract ABI to the abi.json file on our node project. Paste the following code into it: 

[
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [{ "name": "", "type": "string" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "spender", "type": "address" },
      { "name": "value", "type": "uint256" }
    ],
    "name": "approve",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "sender", "type": "address" },
      { "name": "recipient", "type": "address" },
      { "name": "amount", "type": "uint256" }
    ],
    "name": "transferFrom",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "decimals",
    "outputs": [{ "name": "", "type": "uint8" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "spender", "type": "address" },
      { "name": "addedValue", "type": "uint256" }
    ],
    "name": "increaseAllowance",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "value", "type": "uint256" }],
    "name": "burn",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{ "name": "account", "type": "address" }],
    "name": "balanceOf",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [{ "name": "", "type": "string" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "spender", "type": "address" },
      { "name": "subtractedValue", "type": "uint256" }
    ],
    "name": "decreaseAllowance",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "recipient", "type": "address" },
      { "name": "amount", "type": "uint256" }
    ],
    "name": "transfer",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      { "name": "owner", "type": "address" },
      { "name": "spender", "type": "address" }
    ],
    "name": "allowance",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      { "name": "name", "type": "string" },
      { "name": "symbol", "type": "string" },
      { "name": "decimals", "type": "uint8" },
      { "name": "totalSupply", "type": "uint256" },
      { "name": "feeReceiver", "type": "address" },
      { "name": "tokenOwnerAddress", "type": "address" }
    ],
    "payable": true,
    "stateMutability": "payable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "from", "type": "address" },
      { "indexed": true, "name": "to", "type": "address" },
      { "indexed": false, "name": "value", "type": "uint256" }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "owner", "type": "address" },
      { "indexed": true, "name": "spender", "type": "address" },
      { "indexed": false, "name": "value", "type": "uint256" }
    ],
    "name": "Approval",
    "type": "event"
  }
]

Now, we can create an instance of the SHIBA INU contract using this json interface that will allow us to interact with its functions. At the end of our utils.py, create a new instance for the contract using each node provider. 

########## SMART CONTRACT INTERACTIONS ##############
CONTRACT_ADDRESS = "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE"
with open("src/abi.json") as f:
    abi = json.load(f)
full_node_contract = full_node_provider.eth.contract(address=CONTRACT_ADDRESS, abi=abi)
archive_node_contract = archive_node_provider.eth.contract(
    address=CONTRACT_ADDRESS, abi=abi
)

A web3.eth.Contract instance contains the interface for every method, event, and public variable in the smart contract. We are interested (at first) in the _functions variable, which will contain a list of dictionaries that holds every function metadata, including its name along with their required inputs. Inside every dictionary, another variable, stateMutability, indicates if this method changes the network state. For read-only methods, the value will be view, which tells us that this method only reads the smart contract state. 

To extract this useful metadata, we will implement a getContractCallMethods function that will only return the names and inputs (in dict-form) of the view functions. At the end of the utils.py file add: 

def get_contract_call_methods():
    pprint(full_node_contract.functions._functions)
    print(type(full_node_contract.functions._functions))
    view_functions = {}
    for function in full_node_contract.functions._functions:
        # print(function)
        if function["stateMutability"] == "view":
            view_functions[function["name"]] = function["inputs"]
    return view_functions

As told before, for this tutorial, we want to interact with our app through CLI, so the inputs required will be prompted to the user using inquirer.   

Inside the src folder, create a new folder called prompts. Then let us create three files:   

  • common_state.py: that will prompt the options to select a common state query.  
  • custom_state.py: this will prompt the options to select a custom smart contract function to query its state, and  
  • common.py: that will contain common functions to utilize on the previous files  

Now, our project folder structure must look like this: 

├── main.py 
└── src 
    ├── abi.json 
    ├── prompts 
    │   ├── common.py 
    │   ├── common_state.py 
    │   └── custom_state.py 
    └── utils.py 

Querying a common state function

Let us focus on querying common state functions first. Every common state function requires some inputs, such as the block and address. Other functions, such as getting the storageAt and block transactions, need some custom inputs, too, so let us focus on writing the scaffolding for this. In our common_state.py file, we will add: 

import inquirer
from src.utils import *
from operator import itemgetter
LATEST_BLOCK = ""
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
# Builds a prompt for block input
def block_prompt():
    LATEST_BLOCK = get_block_number()
    return inquirer.Text(
        "block",
        message="Enter the block number to perform the query (blank to use latest block)",
        default=LATEST_BLOCK,
    )

# Builds a prompt for address input
def address_prompt():
    return inquirer.Text(
        "address",
        message="Enter an address to perfom the query (blank to use zero address)",
        default=ZERO_ADDRESS,
    )

#  Prompts the inputs for the block to query transactions
#  and select if go full details or just the hashes
def get_block_full():
    questions = [
        block_prompt(),
        inquirer.Confirm(
            "full",
            message="Do you wish to get the full transactions output",
            default=False,
        ),
    ]
    return inquirer.prompt(questions)

# Prompts the inputs for the block
# and the address to perform a query
def get_address_block():
    questions = [address_prompt(), block_prompt()]
    return inquirer.prompt(questions)

# Prompts the inputs for the block, address
# and the position to perform a query
def get_address_block_position():
    questions = [
        address_prompt(),
        block_prompt(),
        inquirer.Text(
            "position",
            message="Enter the position of the storage to perform the query (default 0)",
            default="0",
        ),
    ]
    return inquirer.prompt(questions)

We also have some functions that will help us manage these queries declaratively, mainly focused on fetching the results and printing them. Still, in our common_state.py file, add: 

# Performs query for getting ETH balance
def process_eth_balance():
    address, block = itemgetter("address", "block")(get_address_block())
    address = to_checksum_address(address)
    print_eth_balance_of(address, int(block))
    return

# Performs query for getting code at given address
def process_storage_at():
    address, block, position = itemgetter("address", "block", "position")(
        get_address_block_position()
    )
    address = to_checksum_address(address)
    print_storage_at(address, int(position), int(block))

# Performs query for getting storage at an address
# at a given position
def process_code_at():
    address, block = itemgetter("address", "block")(get_address_block())
    address = to_checksum_address(address)
    print_code_at(address, int(block))

# Performs query for getting a block transactions
def process_transactions():
    block, full = itemgetter("block", "full")(get_block_full())
    print_block_transactions(int(block), full)

Finally, we will add the logic required to request the user inputs needed to query a specific function.

# Prompts the option to select the common state
# query function to call
def get_commons_selection():
    return inquirer.prompt(
        [
            inquirer.List(
                "commons",
                message="Select a common query to perform",
                choices=[
                    "Get ETH balance of an address at a given block",
                    "Get storage at an address at a given position and block",
                    "Get code of an address at a given block",
                    "Get block transactions from a given block",
                ],
            )
        ]
    )["commons"]

# Executes the method select on CLI
def process_commons_selection(selection):
    if "balance" in selection:
        process_eth_balance()
    elif "code" in selection:
        process_code_at()
    elif "storage" in selection:
        process_storage_at()
    elif "transactions" in selection:
        process_transactions()

At the end, the common_state.py file shall look like this: 

import inquirer
from src.utils import *
from operator import itemgetter
LATEST_BLOCK = ""
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
# Builds a prompt for block input
def block_prompt():
    LATEST_BLOCK = get_block_number()
    return inquirer.Text(
        "block",
        message="Enter the block number to perform the query (blank to use latest block)",
        default=LATEST_BLOCK,
    )

# Builds a prompt for address input
def address_prompt():
    return inquirer.Text(
        "address",
        message="Enter an address to perfom the query (blank to use zero address)",
        default=ZERO_ADDRESS,
    )

#  Prompts the inputs for the block to query transactions
#  and select if go full details or just the hashes
def get_block_full():
    questions = [
        block_prompt(),
        inquirer.Confirm(
            "full",
            message="Do you wish to get the full transactions output",
            default=False,
        ),
    ]
    return inquirer.prompt(questions)

# Prompts the inputs for the block
# and the address to perform a query
def get_address_block():
    questions = [address_prompt(), block_prompt()]
    return inquirer.prompt(questions)

# Prompts the inputs for the block, address
# and the position to perform a query
def get_address_block_position():
    questions = [
        address_prompt(),
        block_prompt(),
        inquirer.Text(
            "position",
            message="Enter the position of the storage to perform the query (default 0)",
            default="0",
        ),
    ]
    return inquirer.prompt(questions)

# Performs query for getting ETH balance
def process_eth_balance():
    address, block = itemgetter("address", "block")(get_address_block())
    address = to_checksum_address(address)
    print_eth_balance_of(address, int(block))
    return

# Performs query for getting code at given address
def process_storage_at():
    address, block, position = itemgetter("address", "block", "position")(
        get_address_block_position()
    )
    address = to_checksum_address(address)
    print_storage_at(address, int(position), int(block))

# Performs query for getting storage at an address
# at a given position
def process_code_at():
    address, block = itemgetter("address", "block")(get_address_block())
    address = to_checksum_address(address)
    print_code_at(address, int(block))

# Performs query for getting a block transactions
def process_transactions():
    block, full = itemgetter("block", "full")(get_block_full())
    print_block_transactions(int(block), full)

# Prompts the option to select the common state
# query function to call
def get_commons_selection():
    return inquirer.prompt(
        [
            inquirer.List(
                "commons",
                message="Select a common query to perform",
                choices=[
                    "Get ETH balance of an address at a given block",
                    "Get storage at an address at a given position and block",
                    "Get code of an address at a given block",
                    "Get block transactions from a given block",
                ],
            )
        ]
    )["commons"]

# Executes the method select on CLI
def process_commons_selection(selection):
    if "balance" in selection:
        process_eth_balance()
    elif "code" in selection:
        process_code_at()
    elif "storage" in selection:
        process_storage_at()
    elif "transactions" in selection:
        process_transactions()

Querying a custom smart contract

We also want to be able to query a custom smart contract state. Querying smart contracts state is beneficial if we want to get historical data from them. 
First, we want to get the inputs from a user required for querying a function in our smart contract. We need a way to build a custom prompt asking the user for these inputs. Let us do this, bearing in mind that every contract can be different and have distinct functions requiring different inputs. Let us add the following code to our custom_state.py file: 

# Builds a new inquirer prompt for each required
# input for the selected smart contract method
def build_prompt(inputs, block):
    questions = []
    for inputt in inputs:
        questions.append(
            inquirer.Text(
                name=inputt["name"],
                message="Enter an {} for the {} to perform query".format(
                    inputt["type"], inputt["name"]
                ),
                default=get_default_value(inputt["type"]),
            )
        )
    questions.append(
        inquirer.Text(
            name="block",
            message="Enter the block number to perform the query",
            default=block,
        )
    )
    return questions

# Gets the user inputs required to pass as function
# parameters in the method selected for the custom contract
def get_user_input(inputs, block):
    questions = build_prompt(inputs, block)
    answers = inquirer.prompt(questions)
    block = answers["block"]
    del answers["block"]
    return list(answers.values()), block

# Returns the default value of an inquirer prompt
# based on a smart contract function input type
def get_default_value(input_type):
    if "address" in input_type:
        return ZERO_ADDRESS
    elif "uint" in input_type or "int" in input_type or "enum" in input_type:
        return "0"
    elif "bool" in input_type:
        return False
    elif "bytes" in input_type:
        return "0x0"
    elif "array" in input_type:
        return []
    elif "string" in input_type:
        return ""
    return None

These methods will ask the user input required regarding each function and return the function parameters entered by the user in array format. The previous is helpful as we can use argument unpacking to pass them as parameters to the function. 

Finally, as in the common state query prompts, we want a gateway to process the selected option and trigger the desired function to query a state in the smart contract. 

# Returns the option selected to query on a custom
# smart contract. Also returns the contract methods names
# and their required inputs
def get_custom_selection():
    contract_functions = get_contract_call_methods()
    return (
        inquirer.prompt(
            [
                inquirer.List(
                    name="custom",
                    message="Choose a function from the smart contract",
                    choices=contract_functions.keys(),
                )
            ]
        )["custom"],
        contract_functions,
    )

# Performs the selection process for a method
# on the smart contract to query
def process_custom_selection(selection, contract_functions):
    latest_block = get_block_number()
    # print("Required Inputs: ", contract_functions[selection])
    inputs = contract_functions[selection]
    user_inputs, block = get_user_input(inputs, latest_block)
    # print("inputs: ", user_inputs)
    if len(inputs) < 1:
        call_contract_function_without_params(selection, block)
    else:
        call_contract_function_with_params(selection, user_inputs, block)

Important note: Since some Solidity types like structs and mappings require a complex way of passing them as parameters (like tuples), trying to call functions that use them will trigger an error. 

In the end, our custom_state.py file will look like this: 

import inquirer
from .common_state import ZERO_ADDRESS
from src.utils import *
# Builds a new inquirer prompt for each required
# input for the selected smart contract method
def build_prompt(inputs, block):
    questions = []
    for inputt in inputs:
        questions.append(
            inquirer.Text(
                name=inputt["name"],
                message="Enter an {} for the {} to perform query".format(
                    inputt["type"], inputt["name"]
                ),
                default=get_default_value(inputt["type"]),
            )
        )
    questions.append(
        inquirer.Text(
            name="block",
            message="Enter the block number to perform the query",
            default=block,
        )
    )
    return questions

# Gets the user inputs required to pass as function
# parameters in the method selected for the custom contract
def get_user_input(inputs, block):
    questions = build_prompt(inputs, block)
    answers = inquirer.prompt(questions)
    block = answers["block"]
    del answers["block"]
    return list(answers.values()), block

# Returns the default value of an inquirer prompt
# based on a smart contract function input type
def get_default_value(input_type):
    if "address" in input_type:
        return ZERO_ADDRESS
    elif "uint" in input_type or "int" in input_type or "enum" in input_type:
        return "0"
    elif "bool" in input_type:
        return False
    elif "bytes" in input_type:
        return "0x0"
    elif "array" in input_type:
        return []
    elif "string" in input_type:
        return ""
    return None

# Returns the option selected to query on a custom
# smart contract. Also returns the contract methods names
# and their required inputs
def get_custom_selection():
    contract_functions = get_contract_call_methods()
    return (
        inquirer.prompt(
            [
                inquirer.List(
                    name="custom",
                    message="Choose a function from the smart contract",
                    choices=contract_functions.keys(),
                )
            ]
        )["custom"],
        contract_functions,
    )

# Performs the selection process for a method
# on the smart contract to query
def process_custom_selection(selection, contract_functions):
    latest_block = get_block_number()
    # print("Required Inputs: ", contract_functions[selection])
    inputs = contract_functions[selection]
    user_inputs, block = get_user_input(inputs, latest_block)
    # print("inputs: ", user_inputs)
    if len(inputs) < 1:
        call_contract_function_without_params(selection, block)
    else:
        call_contract_function_with_params(selection, user_inputs, block)

Final touches

Now, we need to implement our final prompt that will manage to choose if query common state or a custom smart contract function. The common.py file contains the code to select if a user will query a common state function or one given on the custom smart contract, so it serves as a middleware to decide what to execute next. 

import inquirer
from src.prompts.custom_state import *
from src.prompts.common_state import *
# Prompts the main selection panel
def get_selection():
    questions = [
        inquirer.List(
            "main query",
            message="Select the type of functions to query",
            choices=[
                "Common state functions (get_balance, get_block,...)",
                "Custom contract state functions",
            ],
        )
    ]
    answers = inquirer.prompt(questions)
    if "Common" in answers["main query"]:
        selection = get_commons_selection()
        process_commons_selection(selection)
    else:
        selection, contract_functions = get_custom_selection()
        process_custom_selection(selection, contract_functions)

Finally, and most importantly, our main.py file shall execute the get_selection function to prompt all user inputs. 

from src.utils import * 
from src.prompts import common 
common.get_selection() 

To run a demo of this app, on the projects root folder, we just run: 

python main.py 

If we choose to run common state functions queries, it will ask at least for the block number (it will use latest as default). Some other functions will require an address (it will use the zero address by default). 

Demo of querying the ETH balance of an address on a given block

On the other hand, we could query any state call function from our custom smart contract. The inputs will vary depending on the function to query (some functions will not even need an input).

Demo of querying the totalSupply of the SHIBA-INUJ ERC-20 Tken

As always, the complete code is available in the tutorial’s repository

Conclusion

In this series, we have discovered how to perform, programmatically and interactively, state queries to a given network and a custom smart contract. This scripting tutorial allows fetching helpful information about the actual and historical states of the blockchain in an error-based approach. For example, this tool could perform different queries to a network state using both full and archive nodes. This transition is helpful in situations where the calls to an RPC provider get limited, and every one of them needs to be executed in the most efficient way possible.

Have you already explored what you can achieve with Chainstack? Get started for free today.

Chainstack uses cookies to provide you with a secure and
personalized experience on its website. Learn more.