Understanding How to Read Data of Ethereum Smart Contract | Functional and Blockchain
Tags: Elixir, Ethereum, Rust, Crypto, Blockchain

Understanding How to Read Data of Ethereum Smart Contract | Functional and Blockchain Leeduckgo 6 days ago·5 min read

About This Series

According to show the codes and anlysis the classic articles, I would like to share the thinking and practices about functional programming. The Series will focus on two functional programming language: Rust&Elixir. Maybe I will reference other PL for auxiliary:).

I am going to show the function of reading Ethereum Smart Contract by Elixir&Rust in this article. It’s important that the program is not only working on Ethereum but also any blockchain that is supporting EVM, for example: Moonbeam on Polkadot! @_@

The Code Example of Elixir in this article:

The Code Example of Rust in this article:

0x01 Ethereumex & ExABI

The two repos of Elixir I prefer is Ethereumex:

https://github.com/mana-ethereum/ethereumex Elixir JSON-RPC client for the Ethereum blockchain.

&

ExABI：

https://github.com/poanetwork/ex_abi The Application Binary Interface (ABI) of Solidity describes how to transform binary data to types which the Solidity programming language understands.

Tips for ABI:

ABI (Application Binary Interface) in the context of computer science is an interface between two program modules. It is very similar to API (Application Program Interface), a human-readable representation of a code’s interface. ABI defines the methods and structures used to interact with the binary contract, just like API does but on a lower-level. — —https://www.quicknode.com/guides/solidity/what-is-an-abi The .abi file is including the description of function interfaces and events by json . This is an example ABI for HelloWorld.sol

[{

"constant": true,

"inputs": [],

"name": "get",

"outputs": [{

"name": "",

"type": "string"

}

],

"payable": false,

"stateMutability": "view",

"type": "function"

}]

0x02 Config for Ethereumex

Firstly, let us add Ethereumex to the field of deps and application in mix.exs !

# mix.exs:

def application do

[

mod: {TaiShang.Application, []},

extra_applications: [:logger, :runtime_tools, :ethereumex]

]

end

……

defp deps do

[

{:ethereumex, "~> 0.7.0"}

]

end

Then,in config/config.exs , add Ethereum protocol host params to your config file:

# config.exs

config :ethereumex,

url: "http://localhost:8545" # node url

0x03 Tx Struct

Show in Elixir

It’s very eazy to understanding Struct in Elixir by the code.

The tx of Ethereum showed in Elixir:

%Transaction{

nonce: nonce, # counter to ensure the sequence of txs

gas_price: @gas.price, # gas fee

gas_limit: @gas.limit, # gas gas limit

to: bin_to, # addr in binary

value: 0, # the eth u are going to send

init: <<>>, # bytecode

data: data # the data u are going to send

}

We have just read the data in Ethereum (the writing of data will show in another article), so the nonce is useless. The nonce is needed and changed only when we writing data to the contract.

eth_call Executes a new message call immediately without creating a transaction on the block chain. Example

// Request

curl -X POST --data '{"jsonrpc":"2.0","method":"eth_call","params":[{see above}],"id":1}'

​

// Result

{

"id":1,

"jsonrpc": "2.0",

"result": "0x"

}

The mechanism of gas is not friendly for freshman, so we can set gas_price and gas_limit to a certainly number now:

@gas %{price: 0, limit: 300_000}

Show in Rust

It’s a similar struct in Rust:



let tx = TransactionRequest {

from: accounts[0],

to: Some(accounts[1]),

gas: None, // gaslimit

gas_price: None,

value: Some(U256::from(10000)),

data: None,

nonce: None,

condition: None

}; /// from: https://kauri.io/#collections/A%20Hackathon%20Survival%20Guide/sending-ethereum-transactions-with-rust/ let tx = TransactionRequest {from: accounts[0],to: Some(accounts[1]),gas: None, // gaslimitgas_price: None,value: Some(U256::from(10000)),data: None,nonce: None,condition: None};

Now there are two params of tx we should handle:

to & data.

0x04 String to Binary for Address

The address using in blockchain(such as 0x769699506f972A992fc8950C766F0C7256Df601f ) could be translated to binary in Elixir program:

@spec addr_to_bin(String.t()) :: Binary.t()

def addr_to_bin(addr_str) do

addr_str

|> String.replace("0x", "")

|> Base.decode16!(case: :mixed)

end

0x05 Smart Contract Function to Data

We would like to generate data by string style of Ethereum functions and params list:

@spec get_data(String.t(), List.t()) :: String.t()

def get_data(func_str, params) do

payload =

func_str

|> ABI.encode(params)

|> Base.encode16(case: :lower) "0x" <> payload

end

The examples of “String style of Ethereum functions”:

@func %{

balance_of: "balanceOf(address)",

token_of_owner_by_index: "tokenOfOwnerByIndex(address, uint256)",

token_uri: "tokenURI(uint256)",

get_evidence_by_key: "getEvidenceByKey(string)",

new_evidence_by_key: "newEvidenceByKey(string, string)",

mint_nft: "mintNft(address, string)",

owner_of: "ownerOf(uint256)"

}

The abstract of string style of eth function is “function_name(param_type1, param_type2,…)”

It’s good to go deeper to see the implementation of encode function!

def encode(function_signature, data, data_type \\ :input) # string type of function to function_selector

# then call encode function again with function_selector

def encode(function_signature, data, data_type) when is_binary(function_signature) do

function_signature

|> Parser.parse!()

|> encode(data, data_type)

end def encode(%FunctionSelector{} = function_selector, data, data_type) do

TypeEncoder.encode(data, function_selector, data_type)

end

The Struct of FunctionSelector:

iex(5)> ABI.Parser.parse!("baz(uint8)")

%ABI.FunctionSelector{

function: "baz",

input_names: [],

inputs_indexed: nil,

method_id: nil,

returns: [],

type: nil,

types: [uint: 8]

}

It’s the work of TypeEncoder.encode to compile data , function_selector and data_type tranlate to data . U can see the details in:

0x06 The Translator of Smart Contract Response

It’s good to writing a TypeTransalator to change the hex data to normal data in Elixir for response of Smart Contract:

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

The functions we are going to choose is based on the response’s type, we can fetch it in ABI:

{

"constant": true,

"inputs": [],

"name": "get",

"outputs": [{

"name": "",

"type": "string" # The response is string!

}

],

"payable": false,

"stateMutability": "view",

"type": "function"

}

0x07 The Caller in Elixir

It’s the last step! Just mix the functions above in a mixed function, the data reading of smart contract is working!

For Example — reading the balance of ERC20 token:

@spec balance_of(String.t(), String.t()) :: Integer.t()

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(%{ # the tx is encapsulated by ethereumex.

data: data,

to: contract_addr

}) TypeTranslator.data_to_int(balance_hex)

end

0x08 The Caller in Rust

The last one is the example that calling ethereum by rust-web3 ：

extern crate hex;

use hex_literal::hex; use web3::{

contract::{Contract, Options},

types::{U256, H160, Bytes},

}; #[tokio::main]

async fn main() -> web3::contract::Result<()> {

let _ = env_logger::try_init();

let http = web3::transports::Http::new("https://ropsten.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161")?;

let web3 = web3::Web3::new(http); let addr_u8 = hex::decode("7Ad11de6d4C3DA366BC929377EE2CaFEcC412A10").expect("Decoding failed");

let addr_h160 = H160::from_slice(&addr_u8); let contra = Contract::from_json(

web3.eth(),

addr_h160,

include_bytes!("../contracts/hello_world.json"),

)?; // let acct:[u8; 20] = hex!("f24ff3a9cf04c71dbc94d0b566f7a27b94566cac").into();



let result = contra.query::<String, _, _,_>("get", (), None, Options::default(), None).await?;

println!("{}", result); Ok(())

}

The Code Example of Elixir in this article:

The Code Example of Rust in this article: