DEV Community

Cover image for Interacting with EVM Networks in Python: From RPC Node to Smart Contract Calls
OnlineProxy
OnlineProxy

Posted on

Interacting with EVM Networks in Python: From RPC Node to Smart Contract Calls

Anyone who takes a deep dive into the world of decentralized finance and web3 development eventually reaches a point where simply observing transactions in a blockchain explorer is no longer enough. The question arises: how can I programmatically access this data? How can I automate interactions, track the balances of thousands of wallets, or "listen" to the events of a specific smart contract? The answer lies in understanding the fundamental architecture that connects your code to the decentralized state machine—the blockchain.

Today, we will take a deep dive into this architecture. This is not a simple "copy-paste" guide. Our goal is to dissect the entire interaction chain, from setting up a professional work environment to deciphering the data returned by contracts. We won't just learn to call functions; we'll understand why it works this way and not another. This article is for those who seek not quick fixes, but foundational knowledge that will become the bedrock for creating complex and robust web3 tools in Python.

Framework #1: The Foundation of Your Web3 Toolkit

Before you build a skyscraper, you need to lay a solid foundation. In our case, this is a properly configured environment. The choice of tools here is not random but dictated by the performance, compatibility, and scalability requirements of future applications.

Why Python?
With its clear syntax and vast ecosystem, Python has become the de-facto standard for data analysis, automation, and backend development. In the web3 world, it's valued for its ability to rapidly prototype complex scripts for on-chain analytics, bot creation, and managing multiple accounts. Its asynchronous capabilities allow for efficient handling of network requests to the blockchain, which is critical for performance.

What Libraries Will We Need?

The key element of our stack is the web3.py library. It's a powerful tool that provides a Python interface for interacting with Ethereum-compatible (EVM) networks.

However, herein lies the first "expert" nuance. Web3 development is a dynamic field, and sometimes the latest library versions can have bugs or lack support for recent language updates. As of this writing, for instance, the latest version of web3.py has compatibility issues with Python 3.12. Therefore, the professional approach is to lock in a stable, proven version.

# requirements.txt

web3==6.14.0
curl_cffi==0.6.2
fake-user-agent==1.5.1
Enter fullscreen mode Exit fullscreen mode
  • web3==6.14.0: We explicitly specify the version. This ensures our code works predictably and won't break after an accidental library update. Version 6.14.0 is a stable release that works well with "out-of-the-box" asynchronous features and is compatible with Python 3.11.

  • curl_cffi: A high-performance library for asynchronous HTTP requests. It serves as a reliable alternative to aiohttp, which many developers find problematic due to installation and SSL certificate issues. curl_cffi often demonstrates better performance when handling a large number of concurrent requests to RPC nodes. As an alternative, httpx is also a strong contender.

  • fake-user-agent: Used to spoof the User-Agent in HTTP headers, which helps disguise our scripts as regular browsers and avoid being blocked by some RPC services.

To manage dependencies and isolate the project, we use a virtual environment created with Python 3.11. This is standard practice that prevents conflicts between libraries from different projects.

# Create and activate the virtual environment
python3.11 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

This seemingly simple set of steps is actually a finely-tuned configuration for professional work that will save you from a host of problems down the line.

How Does Our Code "Talk" to the Blockchain?

Imagine the blockchain as a remote, global computer whose state is constantly changing. To send it a command or read data, we need an intermediary. This intermediary is the RPC Node (Remote Procedure Call Node).

An RPC node is a server that "listens" to the blockchain network and provides us with an API to interact with it. It is through the RPC that we send requests like "what is the current balance of this wallet?" or "what is the current gas price?".

The address of an RPC node is a standard URL that we pass into our code. The choice of this URL determines which network we will be working with: Ethereum, Polygon, Arbitrum, or any other EVM-compatible chain. Reliable public RPC nodes can be found on services like Chainlist.org, or you can use specialized providers like Infura or Alchemy.

Let's see how to establish a communication session with the Ethereum blockchain asynchronously:

import asyncio
from web3 import AsyncWeb3

# RPC node URL for the Ethereum network
RPC_URL = "https://rpc.ankr.com/eth"

async def main():
    # 1. Create an AsyncWeb3 object, specifying the provider
    w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(RPC_URL))

    # 2. Check if the connection is successful
    is_connected = await w3.is_connected()
    print(f"Connection to network established: {is_connected}")

    if is_connected:
        # 3. Get basic network parameters
        chain_id = await w3.eth.chain_id
        latest_block = await w3.eth.block_number
        gas_price = await w3.eth.gas_price

        print(f"Chain ID: {chain_id}")
        print(f"Latest Block: {latest_block}")

        # Convert gas price from Wei to Gwei for readability
        gas_price_gwei = w3.from_wei(gas_price, 'gwei')
        print(f"Gas Price: {gas_price_gwei} Gwei")

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

In this code, the w3 object is our main gateway for all interactions. It encapsulates the logic for communicating with the RPC node. Through w3.eth, we gain access to methods related to the state of the Ethereum blockchain. Note the use of await—all network calls are asynchronous, which prevents our application from blocking while waiting for a response from the node.

Using an asynchronous approach from the very beginning is a strategic decision. It allows for easy scaling in the future to work with hundreds or thousands of accounts or tasks in parallel using asyncio.gather.

Framework #2: ABI as a Communication Protocol

We've learned how to get general data about the network. But how do we interact with a specific smart contract, like the USDT token? How do we call its balanceOf function and pass a wallet address to it?

This is where one of the key concepts of the EVM comes into play: the A*BI (Application Binary Interface)*.

Simply put, the ABI is a JSON-formatted description of a smart contract's public interface. It is a kind of "API documentation" for the smart contract that tells our Python code:

  • What functions exist in the contract (name, symbol, balanceOf, transfer).
  • What these functions are called.
  • What arguments (and of what types) they take as input (e.g., balanceOf takes one argument of type address).
  • What (and of what type) they return as output (e.g., balanceOf returns a number of type uint256).

The ABI is the bridge, the "Rosetta Stone," that allows web3.py to translate our Python call contract.functions.balanceOf(my_address).call()into the low-level binary code that the EVM understands. Without an ABI, interacting with a contract's functions would be nearly impossible.

Where to Get the ABI?

The ABI of any verified contract can be found in a blockchain explorer (like Etherscan or Arbiscan) on the contract's page, under the Contract tab.

An important point: many modern contracts, especially tokens, use the proxy pattern to allow for upgrades. This means the address you interact with (the proxy contract) only stores data, while all the logic is delegated to another contract (the implementation). In such cases, you must get the ABI from the implementation contract's page, which can be reached via the Read as Proxy or Write as Proxy tab in the explorer.

Step-by-Step Guide: Reading Data from a Token's Smart Contract

Let's combine everything we've learned and write a script that gets the name, symbol, and balance of the USDC token on the Arbitrum network for a specific wallet.

Step 1: Preparation
We already have our working environment. Let's create a folder named abi and save the ABI of the USDC contract into it as a file named erc20.json.

Why erc20.json? All fungible tokens (USDT, USDC, DAI) follow a single standard: ERC-20. This standard dictates that they must have a common set of basic functions (name, symbol, decimals, balanceOf, approve, transfer, etc.). This means they all share the same basic ABI (with rare exceptions). This property, similar to polymorphism in OOP, is incredibly powerful: by writing code to work with one ERC-20 token, we can use it for thousands of others simply by changing the contract address.

Step 2: Writing the Code

import asyncio
import json
from web3 import AsyncWeb3

# --- Constants and Configuration ---
ARB_RPC_URL = "https://arbitrum.llamarpc.com"
# USDC contract address on Arbitrum
USDC_CONTRACT_ADDRESS = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
# Wallet address whose balance we want to check
WALLET_ADDRESS = "0x4D062aDE39d33A037a3429367F4779434407c645"
# Path to the ABI file
ERC20_ABI_PATH = "abi/erc20.json"


def load_abi(path: str):
    """Function to load ABI from a JSON file."""
    with open(path, 'r') as f:
        return json.load(f)

async def main():
    w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(ARB_RPC_URL))
    if not await w3.is_connected():
        print("Error connecting to RPC")
        return

    # --- Interacting with the Smart Contract ---

    # 1. Load the ABI
    erc20_abi = load_abi(ERC20_ABI_PATH)

    # 2. Convert addresses to checksum format for reliability
    # This is standard practice that protects against case-sensitivity errors
    contract_address_checksum = w3.to_checksum_address(USDC_CONTRACT_ADDRESS)
    wallet_address_checksum = w3.to_checksum_address(WALLET_ADDRESS)

    # 3. Create an instance of the contract object
    # This object "knows" its address and how to talk to it (via the ABI)
    usdc_contract = w3.eth.contract(address=contract_address_checksum, abi=erc20_abi)

    # 4. Call the contract's read functions
    # .functions - access all functions
    # .balanceOf(...) - select the desired function and pass arguments
    # .call() - execute the call (this is a read operation, it's free)

    token_name = await usdc_contract.functions.name().call()
    token_symbol = await usdc_contract.functions.symbol().call()
    token_decimals = await usdc_contract.functions.decimals().call()
    raw_balance = await usdc_contract.functions.balanceOf(wallet_address_checksum).call()

    # 5. Process the result
    # The balance is returned as an integer; it must be adjusted by the decimals
    actual_balance = raw_balance / (10 ** token_decimals)

    print(f"Token Information:")
    print(f"  Name: {token_name}")
    print(f"  Symbol: {token_symbol}")
    print(f"  Decimals: {token_decimals}")
    print(f"Balance of wallet {WALLET_ADDRESS}: {actual_balance} {token_symbol}")


if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

This example demonstrates the entire chain: connecting to the network, creating a contract object using its address and ABI, and finally, calling its read functions.

Framework #3: The Concept of Decimals and the Integer-Only World of the EVM

In the example above, we received a raw_balance, a huge integer, and then divided it by 10 ** token_decimals. Why is that?

This brings us to our last fundamental principle for today. The EVM (Ethereum Virtual Machine) does not work with floating-point numbers. This design choice ensures determinism: operations with floating-point numbers can yield microscopically different results on different computers, which is unacceptable in a system where every node in the network must arrive at an absolutely identical state.

All calculations in the EVM are done with integers. But how, then, do we represent a value like 1.5 USDC?

The solution is a convention. Tokens store and transfer amounts as large integers, and to interpret them correctly, a parameter called decimals is introduced.

  • decimals is a number that indicates how many places to shift the decimal point to the left to get the value we are used to.

For example:

  • Native ETH (and most tokens in the Ethereum ecosystem) has decimals = 18. A balance of 1,000,000,000,000,000,000 Wei is actually equal to 1 ETH.

  • USDT and USDC tokens have decimals = 6. A balance of 1,500,000 "atomic units" is equal to 1.5 USDC.

Therefore, when we get a balance from a contract, we always receive an integer (raw_balance). It is our job as off-chain developers to query the contract for its decimals and correctly convert this "raw" value into a human-readable format. Ignoring decimals is one of the most common mistakes beginners make, leading to a completely incorrect interpretation of on-chain data.

Final Thoughts

We have broken down three fundamental frameworks for interacting with EVM networks:

  1. A Professional Environment: Choosing specific versions of Python and its libraries, along with the mandatory use of asynchronicity, are not minor details but the foundation for building stable and scalable applications.
  2. ABI as a Protocol: Understanding that the ABI is the formalized language of communication between your code and a smart contract is the key to working with any decentralized application, no matter how complex.
  3. The EVM's Integer-Only World: The concept of decimals is not a quirk but a fundamental consequence of the EVM's design. The ability to correctly work with large numbers and decimals is what separates a professional from an amateur.

By mastering these principles, you cease to be a mere observer. You gain the ability to programmatically analyze, automate, and interact with any EVM network. The entire decentralized world becomes an open book of data for you.

And the main question you should ask yourself now is: if you can read any state from any contract, what hidden patterns can you discover? What inefficiencies can you find? What new products can you create? The answers to these questions are limited only by your imagination.

Top comments (0)