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! @_@
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"
}]
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
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.
Executes a new message call immediately without creating a transaction on the blockchain.
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
DATA
- the return value of the executed contract.
// 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}
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.
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
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:
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"
}
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
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