Understanding Ethereum Contract Data Reading Process | Functional Programming & Blockchain (Part 2)

·

In the rapidly evolving world of blockchain development, mastering the interaction between programming languages and decentralized networks is essential. This article dives deep into the mechanics of reading data from Ethereum smart contracts using Elixir, a functional programming language gaining traction in blockchain applications. The principles discussed here apply not only to Ethereum but also to any EVM-compatible blockchain such as FISCO BCOS or Moonbeam.

Whether you're building NFT platforms, DeFi protocols, or next-generation Web3 infrastructure, understanding how to efficiently read contract state is foundational. We'll explore real code implementations, break down core concepts like ABI encoding and JSON-RPC calls, and show how functional programming enhances reliability and clarity in blockchain interactions.

Core Concepts: Ethereumex and ExABI

To interact with Ethereum from Elixir, two critical libraries are used: Ethereumex and ExABI.

👉 Discover how developers use Elixir for secure, scalable blockchain interactions.

Ethereumex – The JSON-RPC Gateway

Ethereumex is an Elixir client that communicates with Ethereum nodes via JSON-RPC. It allows your application to send requests like eth_call, eth_getBalance, and more without managing low-level HTTP details.

To use it, add the dependency in your mix.exs:

defp deps do
  [
    {:ethereumex, "~> 0.7.0"}
  ]
end

Also ensure it's included in your application supervision tree:

def application do
  [
    mod: {TaiShang.Application, []},
    extra_applications: [:logger, :runtime_tools, :ethereumex]
  ]
end

Configure the node URL in config.exs:

config :ethereumex,
  url: "http://localhost:8545"

This setup enables seamless communication with local or remote Ethereum nodes.

ExABI – Decoding the Contract Interface

Smart contracts expose functions and events through the Application Binary Interface (ABI) — a JSON format describing method signatures and parameter types.

For example, a simple "Hello World" contract might have this ABI entry:

{
  "constant": true,
  "inputs": [],
  "name": "get",
  "outputs": [{ "name": "", "type": "string" }],
  "stateMutability": "view",
  "type": "function"
}

The ExABI library helps encode function calls and decode return values according to this specification. It transforms high-level Elixir data into binary payloads that the EVM can interpret.

Transaction Structure for Read Operations

Even when reading data, Ethereum uses a transaction-like structure. In Elixir, we represent this as a %Transaction{} struct:

%Transaction{
  gas_price: @gas.price,
  gas_limit: @gas.limit,
  to: bin_to,
  value: 0,
  data: data
}

Note: Unlike write operations, reads don’t require a nonce since they don’t alter state.

Using eth_call for State Queries

The eth_call JSON-RPC method executes a message call locally without broadcasting a transaction. It’s perfect for querying contract state.

Parameters include:

It returns the raw output in hexadecimal format, which must be decoded based on the expected return type.

Converting Addresses to Binary Format

Blockchain addresses like 0x769699506f972A992fc8950C766F0C7256Df601f must be converted to binary for processing:

def addr_to_bin(addr_str) do
  addr_str
  |> String.replace("0x", "")
  |> Base.decode16!(case: :mixed)
end

This function strips the 0x prefix and decodes the hexadecimal string into raw binary — the format required by ExABI for encoding.

Generating Call Data from Function Signatures

To invoke a contract function, you need to generate the data field by combining the function selector and encoded parameters.

def get_data(func_str, params) do
  payload =
    func_str
    |> ABI.encode(params)
    |> Base.encode16(case: :lower)
  "0x" <> payload
end

Function signatures follow the pattern: "functionName(type1, type2)". For example:

Under the hood, ABI.encode/2 parses the function string into a FunctionSelector, then encodes the parameters according to ABI rules.

Decoding Contract Response Data

Raw responses from eth_call come back as hex strings. You must decode them based on the expected return type.

Here’s a utility module for common types:

defmodule Utils.TypeTranslator do
  def data_to_int(raw) do
    raw
    |> hex_to_bin()
    |> ABI.TypeDecoder.decode_raw([{:uint, 256}])
    |> List.first()
  end

  def data_to_str(raw) do
    raw
    |> hex_to_bin()
    |> ABI.TypeDecoder.decode_raw([:string])
    |> List.first()
  end

  def data_to_addr(raw) do
    addr_bin =
      raw
      |> hex_to_bin()
      |> ABI.TypeDecoder.decode_raw([:address])
      |> List.first()
    "0x" <> Base.encode16(addr_bin, case: :lower)
  end
end

Use the contract’s ABI to determine which decoder to apply.

👉 Learn how top teams decode blockchain data efficiently and securely.

Full Example: Reading Token Balance in Elixir

Putting it all together, here's a complete function to read an ERC-20 token balance:

def balance_of(contract_addr, addr_str) do
  {:ok, addr_bytes} = TypeTranslator.hex_to_bytes(addr_str)
  data = get_data("balanceOf(address)", [addr_bytes])

  {:ok, balance_hex} =
    Ethereumex.HttpClient.eth_call(%{
      data: data,
      to: contract_addr
    })

  TypeTranslator.data_to_int(balance_hex)
end

This function:

  1. Converts the owner address to binary
  2. Encodes the balanceOf call with the target address
  3. Sends the request via eth_call
  4. Decodes the result as an integer

Rust Alternative: Using web3.rs

For comparison, here's how you’d perform a similar read operation in Rust using web3.rs:

use web3::{
    contract::{Contract, Options},
    types::{H160, U256},
};
#[tokio::main]
async fn main() -> web3::contract::Result<()> {
    let http = web3::transports::Http::new("https://ropsten.infura.io/v3/YOUR_KEY")?;
    let web3 = web3::Web3::new(http);

    let addr = H160::from_slice(&hex::decode("7Ad1...").unwrap());
    let contract = Contract::from_json(
        web3.eth(),
        addr,
        include_bytes!("../contracts/hello_world.json"),
    )?;

    let result: String = contract.query("get", (), None, Options::default(), None).await?;
    println!("{}", result);
    Ok(())
}

While Rust offers performance and safety, Elixir excels in concurrency and fault tolerance — ideal traits for handling multiple blockchain queries.

Keywords Identified:

Frequently Asked Questions

Q: Can I use this method on non-Ethereum blockchains?
A: Yes! Any EVM-compatible chain (like BSC, Polygon, Moonbeam) supports the same JSON-RPC interface and ABI standards.

Q: Do read operations cost gas?
A: No. eth_call is executed locally by the node and does not consume gas since it doesn’t change blockchain state.

Q: Why use Elixir for blockchain development?
A: Elixir offers high concurrency, fault tolerance, and expressive functional syntax — making it well-suited for handling distributed systems like blockchains.

Q: What is the role of ABI in contract interaction?
A: The ABI defines how to encode function calls and decode responses. Without it, your app wouldn’t know how to communicate with the contract.

Q: Is direct JSON-RPC preferred over libraries like Web3.js?
A: For backend services requiring stability and control, direct RPC with typed languages (Elixir/Rust) often provides better reliability than JavaScript wrappers.

Q: How do I handle different return types dynamically?
A: Parse the ABI JSON first to inspect output types, then route decoding logic accordingly using pattern matching or type dispatch.

👉 See how modern platforms streamline Ethereum contract interactions at scale.