paint-brush
How Rust and Elixir Read Ethereum and other EVM Smart Contracts: Functional Programming & Blockchainby@leeduckgo
1,722 reads
1,722 reads

How Rust and Elixir Read Ethereum and other EVM Smart Contracts: Functional Programming & Blockchain

by leeduckgoSeptember 12th, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The Series will focus on two functional programming languages: Rust&Elixir. I would like to share the thinking and practices of functional programming. The two repos of Elixir I prefer is **[Ethereumex](https://://github.com/mana-ethereum/ethereumex)**: Elixir client for the. Ethereum Smart Contract. Elixir is Elixir’s Elixir JSON-RPC client for. the. the Ethereum blockchain. Elixir will show the function of reading the. contract by Elixir&Rust in this article.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How Rust and Elixir Read Ethereum and other EVM Smart Contracts: Functional Programming & Blockchain
leeduckgo HackerNoon profile picture


The Series will focus on two functional programming languages: Rust&Elixir. I would like to share the thinking and practices of functional programming.


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! @_@

0x01 Ethereumex & ExABI

The two repos of Elixir I prefer is Ethereumex: Elixir JSON-RPC client for the Ethereum blockchain.

&

ExABI: 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 depsand 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 easy 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 blockchain.

Parameters
  • Object - The transaction call object

  • from: DATA, 20 Bytes - (optional) The address the transaction is sent from

  • to: DATA, 20 Bytes - The address the transaction is directed to

  • gas: QUANTITY - (optional) Integer of the gas provided for the transaction execution eth_call consumes zero gas, but this parameter may be needed by some executions

  • gasPrice: QUANTITY - (optional) Integer of the gasPrice used for each paid gas

  • value: QUANTITY - (optional) Integer of the value sent with this transaction

  • data: DATA - (optional) Hash of the method signature and encoded parameters

  • QUANTITY|TAG - integer block number, or the string "latest", "earliest" or "pending", see the default block parameter


For details see Ethereum Contract ABI in the Solidity documentation

Returns

DATA - the return value of the executed contract.

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"
}

——https://eth.wiki/json-rpc/API

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

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

Show in Rust

It's a similar struct in Rust:

/// 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, // gaslimit
        gas_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 translate to data.


You can see the details here:

0x06 The Translator of Smart Contract Response

It's good to write 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 the 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: here | The Code Example of Rust in this article: here