defmodule Etherex do
@moduledoc """
Elixir high level library for the Ethereum blockchain. This library
is based on the methods of the Json RPC API
(https://eth.wiki/json-rpc/api).
All functions returns `{:error, :econnrefused}` if no client is
listening in the configured URL.
You can directly use Elixir integers for quantities or atoms for
tags. The library encode/decode Elixir primitive data to/from the
blockchain.
Compiled code and contract instances are abstracted. The library can
compile code and generate a value of type `t:compiled_code/0` and
that representation can be used to deploy the contact and get a
value of `t:contract/0` that can be used in `call/5` and
`call_transaction/5`.
Some operations allow options. Options are `t:Keyword.t/0` that
accepts optional data accepted by the API (see
https://eth.wiki/json-rpc/api for details when no details are given
in this documentation).
The result of some operations are maps with a direct representation
of the returns of the operations in the API (see
https://eth.wiki/json-rpc/api for details when no details are given
in this documentation).
"""
alias Ethereumex.HttpClient, as: JsonRPC
alias Etherex.Contract
import Etherex.Helpers,
only: [
add_opts: 2,
decode_json_rpc_error: 1,
decode_quantity: 1,
encode_block_parameter: 1
]
alias Etherex.Type
@type address() :: Type.address()
@type hash() :: Type.hash()
@type hex() :: Type.hex()
@type tag() :: Type.tag()
@type quantity() :: Type.quantity()
@type unit() :: Type.unit()
@type error() :: Type.error()
@type event() :: Type.event()
@type opts() :: Type.opts()
@type block_parameter() :: Type.block_parameter()
@opaque bytecode() :: Contract.bytecode()
@opaque contract() :: Contract.contract()
@doc """
Returns the current client version.
"""
@spec client_version() :: {:ok, String.t()} | {:error, error()}
def client_version() do
case JsonRPC.web3_client_version() do
{:ok, version} -> {:ok, version}
error -> decode_json_rpc_error(error)
end
end
@doc """
Works like `client_version/0` but a exception is raised on error.
"""
@spec client_version!() :: String.t()
def client_version!() do
{:ok, v} = client_version()
v
end
@doc """
Returns syncing status.
"""
@spec syncing() :: {:ok, map() | nil} | {:error, error()}
def syncing() do
case JsonRPC.eth_syncing() do
{:ok, false} ->
{:ok, nil}
{:ok, status} ->
{:ok, status |> decode_quantities()}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Works like `syncing/0` but a exception is raised on error.
"""
@spec syncing!() :: map()
def syncing!() do
{:ok, status} = syncing()
status
end
@doc """
Returns the current network id.
"""
@spec net_version() :: {:ok, String.t()} | {:error, error()}
def net_version() do
case JsonRPC.net_version() do
{:ok, version} -> {:ok, version}
error -> decode_json_rpc_error(error)
end
end
@doc """
Works like `net_version/0` but a exception is raised on error.
"""
@spec net_version!() :: String.t()
def net_version!() do
{:ok, v} = net_version()
v
end
@doc """
Returns true if client is actively listening for network connections, false otherwise.
"""
@spec net_listening() :: {:ok, boolean()} | {:error, error()}
def(net_listening()) do
case JsonRPC.net_listening() do
{:ok, listening} -> {:ok, listening}
error -> decode_json_rpc_error(error)
end
end
@doc """
Works like `net_listening/0` but a exception is raised on error.
"""
@spec net_listening!() :: boolean()
def net_listening!() do
case net_listening() do
{:ok, listening} -> listening
end
end
@doc """
Returns the current ethereum protocol version.
"""
@spec protocol_version() :: {:ok, String.t()} | {:error, error()}
def protocol_version() do
case JsonRPC.eth_protocol_version() do
{:ok, version} -> {:ok, version}
error -> decode_json_rpc_error(error)
end
end
@doc """
Works like `protocol_version/0` but a exception is raised on error.
"""
@spec protocol_version!() :: String.t()
def protocol_version!() do
{:ok, version} = protocol_version()
version
end
@doc """
Returns a list of addresses owned by client.
"""
@spec accounts() :: {:ok, [address()]} | {:error, error()}
def accounts() do
case JsonRPC.eth_accounts() do
{:ok, accounts} -> {:ok, accounts}
error -> decode_json_rpc_error(error)
end
end
@doc """
Works like `accounts/0` but a exception is raised on error.
"""
@spec accounts!() :: [address()]
def accounts!() do
{:ok, accounts} = accounts()
accounts
end
@doc """
Returns the balance of the account of given address.
"""
@spec get_balance(address(), block_parameter()) :: {:ok, quantity()} | {:error, error()}
def get_balance(address, block \\ :latest) do
case JsonRPC.eth_get_balance(address, encode_block_parameter(block)) do
{:ok, balance} -> {:ok, balance |> decode_quantity}
error -> decode_json_rpc_error(error)
end
end
@doc """
Works like `get_balance/2` but a exception is raised on error.
"""
@spec get_balance!(address(), block_parameter()) :: quantity()
def get_balance!(address, block \\ :latest) do
{:ok, balance} = get_balance(address, block)
balance
end
@doc """
Transfer funds.
"""
@spec transfer(
from :: address(),
to :: address(),
value :: quantity(),
opts :: opts()
) ::
{:ok, hash()} | {:error, error()}
def transfer(from, to, value, opts \\ []) do
%{from: from, to: to, value: value}
|> add_opts(opts)
|> JsonRPC.eth_send_transaction()
|> case do
{:ok, hash} ->
{:ok, hash}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Deploys a contract to the blockchain.
## Examples
iex> owner = List.last(Etherex.accounts!())
iex> {:ok, _contract} = Etherex.compile_solidity!("priv/solidity/Counter.sol")
...> |> Etherex.deploy(owner, [], gas: 1_000_000)
"""
@spec deploy(
contract :: bytecode(),
creator :: address(),
arguments :: list(),
opts :: opts()
) :: {:ok, contract()} | {:error, error()}
def deploy(contract, creator, arguments, opts \\ []) do
Contract.deploy(contract, creator, arguments, opts)
end
@doc """
Works like `deploy/4` but a exception is raised on error.
"""
@spec deploy!(
code :: bytecode(),
creator :: address(),
arguments :: list(),
opts :: opts()
) :: contract()
def deploy!(code, creator, arguments, opts \\ []) do
{:ok, contract} = deploy(code, creator, arguments, opts)
contract
end
@doc """
Performs a call to a function of a deployed contract. This call does not
generate a transaction in the blockchain.
## Examples
iex> owner = List.last(Etherex.accounts!())
iex> {:ok, [value]} = Etherex.compile_solidity!("priv/solidity/Counter.sol")
...> |> Etherex.deploy!(owner, [], gas: 1_000_000)
...> |> Etherex.call(owner, "get", [])
iex> value
0
"""
@spec call(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
block :: block_parameter(),
opts :: Keyword.t()
) :: {:ok, any()} | {:error, error()}
def call(contract, caller, function, arguments, block \\ :latest, opts \\ []) do
Contract.call(contract, caller, function, arguments, block, opts)
end
@doc """
Works like `call/6` but a exception is raised on error.
"""
@spec call!(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
block :: block_parameter(),
opts :: Keyword.t()
) :: any()
def call!(contract, caller, function, arguments, block \\ :latest, opts \\ []) do
{:ok, result} = call(contract, caller, function, arguments, block, opts)
result
end
@doc """
Generates and returns an estimate of how much gas is necessary to
allow the transaction to complete. The transaction will not be added
to the blockchain. Note that the estimate may be significantly more
than the amount of gas actually used by the transaction, for a
variety of reasons including EVM mechanics and node performance.
## Examples
iex> owner = List.last(Etherex.accounts!())
iex> {:ok, estimated_gas} = Etherex.compile_solidity!("priv/solidity/Counter.sol")
...> |> Etherex.deploy!(owner, [], gas: 1_000_000)
...> |> Etherex.estimate_gas(owner, "get", [])
iex> is_integer(estimated_gas)
true
"""
@spec estimate_gas(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
opts :: opts()
) :: {:ok, any()} | {:error, error()}
def estimate_gas(contract, caller, function, arguments, opts \\ []) do
Contract.estimate_gas(contract, caller, function, arguments, opts)
end
@doc """
Works like `estimate_gas/5` but a exception is raised on error.
"""
@spec estimate_gas!(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
opts :: Keyword.t()
) :: any()
def estimate_gas!(contract, caller, function, arguments, opts \\ []) do
{:ok, result} = estimate_gas(contract, caller, function, arguments, opts)
result
end
@doc """
Generates and returns an estimate the gas cost of a transaction. It
uses `estimate_gas/5` and `gas_price/0`.
## Examples
iex> owner = List.last(Etherex.accounts!())
iex> {:ok, gas_cost} = Etherex.compile_solidity!("priv/solidity/Counter.sol")
...> |> Etherex.deploy!(owner, [], gas: 1_000_000)
...> |> Etherex.estimate_gas_cost(owner, "get", [])
iex> is_integer(gas_cost)
true
"""
@spec estimate_gas_cost(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
opts :: opts()
) :: {:ok, any()} | {:error, error()}
def estimate_gas_cost(contract, caller, function, arguments, opts \\ []) do
with {:ok, estimated_gas} <- estimate_gas(contract, caller, function, arguments, opts),
{:ok, gas_price} <- gas_price() do
{:ok, estimated_gas * gas_price}
else
error -> error
end
end
@doc """
Works like `estimate_gas_cost/5` but a exception is raised on error.
"""
@spec estimate_gas_cost!(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
opts :: Keyword.t()
) :: any()
def estimate_gas_cost!(contract, caller, function, arguments, opts \\ []) do
{:ok, result} = estimate_gas_cost(contract, caller, function, arguments, opts)
result
end
@doc """
Performs a call to a function of a deployed contract, This call generates a
transaction in the blockchain.
## Examples
iex> owner = List.last(Etherex.accounts!())
iex> {:ok, "0x" <> _} = Etherex.compile_solidity!("priv/solidity/Counter.sol")
...> |> Etherex.deploy!(owner, [], gas: 1_000_000)
...> |> Etherex.call_transaction(owner, "get", [])
"""
@spec call_transaction(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
opts :: opts()
) :: {:ok, hash()} | {:error, error()}
def call_transaction(contract, caller, function, arguments, opts \\ []) do
Contract.call_transaction(contract, caller, function, arguments, opts)
end
@doc """
Works like `call_transaction/5` but a exception is raised on error.
"""
@spec call_transaction!(
contract :: contract(),
caller :: address(),
function :: String.t(),
arguments :: list(),
opts :: opts()
) :: hash()
def call_transaction!(contract, caller, function, arguments, opts \\ []) do
{:ok, hash} = call_transaction(contract, caller, function, arguments, opts)
hash
end
@doc """
Returns the information about a transaction requested by transaction
hash.
"""
@spec get_transaction(hash) :: {:ok, map()} | {:error, error}
def get_transaction(hash) do
case JsonRPC.eth_get_transaction_by_hash(hash) do
{:ok, transaction} ->
{:ok, decode_transaction(transaction)}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Works like `get_transaction/1` but a exception is raised on error.
"""
@spec get_transaction!(hash) :: map()
def get_transaction!(hash) do
{:ok, transaction} = get_transaction(hash)
transaction
end
@doc """
Returns the receipt of a transaction by transaction hash. The
receipt is not available for pending transactions and `{:ok, nil}` is
returned.
"""
@spec get_transaction_receipt(Type.hash()) :: {:ok, map() | nil} | {:error, Type.error()}
def get_transaction_receipt(hash) do
case JsonRPC.eth_get_transaction_receipt(hash) do
{:ok, receipt} ->
{:ok,
receipt
|> decode_quantities([
"blockNumber",
"cumulativeGasUsed",
"gasUsed",
"status",
"transactionIndex"
])}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Works like `get_transaction_receipt/1` but a exception is raised on error.
"""
@spec get_transaction_receipt!(hash()) :: map() | nil
def get_transaction_receipt!(hash) do
{:ok, receipt} = get_transaction_receipt(hash)
receipt
end
@doc """
Returns the gas cost of a transaction. If the transaction hasn't
been processed yet (is pending), `{:ok, nil}` is returned.
"""
@spec gas_cost(hash()) :: {:ok, quantity() | nil} | {:error, error()}
def gas_cost(hash) do
with {:ok, receipt} <- get_transaction_receipt(hash),
{:ok, transaction} <- get_transaction(hash) do
gas_used = receipt["gasUsed"]
if is_nil(gas_used) do
{:ok, nil}
else
{:ok, gas_used * transaction["gasPrice"]}
end
else
error ->
error
end
end
@doc """
Works like `gas_cost/1` but a exception is raised on error.
"""
@spec gas_cost!(hash()) :: quantity() | nil
def gas_cost!(hash) do
{:ok, gas_cost} = gas_cost(hash)
gas_cost
end
@doc """
Given a contract and a transaction of the contract, retrieves the
list of events logged in it. Returns `[]` if no events. Returns
`nil` if transaction is pending.
"""
@spec get_events(contract() | bytecode(), hash()) :: {:ok, [event()] | nil} | {:error, error()}
def get_events(contract_or_bytecode, hash) do
Contract.get_events(contract_or_bytecode, hash)
end
@doc """
Works like `get_events/2` but a exception is raised on error.
"""
@spec get_events!(contract(), hash()) :: [event()] | nil
def get_events!(contract, hash) do
{:ok, events} = get_events(contract, hash)
events
end
@doc """
Returns the current price per gas in wei.
"""
@spec gas_price() :: {:ok, quantity()} | {:error, error()}
def gas_price do
case JsonRPC.eth_gas_price() do
{:ok, price} ->
{:ok, price |> decode_quantity}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Works like `gas_price/0` but a exception is raised on error.
"""
@spec gas_price!() :: quantity()
def gas_price!() do
{:ok, price} = gas_price()
price
end
@doc """
Returns the number of most recent block.
"""
@spec block_number() :: {:ok, quantity()} | {:error, error()}
def block_number() do
case JsonRPC.eth_block_number() do
{:ok, block_number} ->
{:ok, block_number |> decode_quantity}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Works like `block_number/0` but a exception is raised on error.
"""
@spec block_number!() :: quantity()
def block_number! do
{:ok, block_number} = block_number()
block_number
end
@doc """
Returns information about a block by block number or by block hash.
"""
@spec get_block(hash() | block_parameter()) :: {:ok, map()} | {:error, error()}
def get_block(number_or_hash \\ :latest) do
cond do
is_binary(number_or_hash) ->
JsonRPC.eth_get_block_by_hash(number_or_hash, true)
true ->
JsonRPC.eth_get_block_by_number(encode_block_parameter(number_or_hash), true)
end
|> case do
{:ok, block} ->
{:ok,
block
|> decode_quantities([
"difficulty",
"gasLimit",
"gasUsed",
"number",
"size",
"timestamp",
"totalDifficulty"
])
|> Map.update!("transactions", fn transactions ->
for transaction <- transactions, do: decode_transaction(transaction)
end)}
error ->
decode_json_rpc_error(error)
end
end
@doc """
Works like `get_block/1` but a exception is raised on error.
"""
@spec get_block!(quantity() | hash() | tag()) :: map()
def get_block!(number_or_hash) do
{:ok, block} = get_block(number_or_hash)
block
end
@doc """
Returns the byte code after compiling Solidity source
code. Parameter `source_or_filename` can be the source code or a
filename with the source code.
If source code is passed, the function tries to send the code to the
client to get the compiled contract.
If a filename is passed, a local compiler is used to compile the
code. If no local compiler can be used or compilation fails then the
source code in file is sent to the client.
"""
@spec compile_solidity(String.t()) :: {:ok, bytecode()} | {:error, error()}
def compile_solidity(source_or_filename) do
Contract.compile_solidity(source_or_filename)
end
@doc """
Works like `compile_solidity/1` but a exception is raised on error.
"""
@spec compile_solidity!(String.t()) :: bytecode()
def compile_solidity!(source_or_filename) do
{:ok, bytecode} = compile_solidity(source_or_filename)
bytecode
end
@doc """
Returns the contract creator address.
"""
@spec get_creator(contract()) :: address()
def get_creator(contract), do: Contract.get_creator(contract)
@doc """
Returns the deployed bytecode of a contract.
"""
@spec get_bytecode(contract()) :: bytecode()
def get_bytecode(contract), do: Contract.get_bytecode(contract)
@doc """
Returns the hash of the creation transaction of a contract.
"""
@spec get_contract_creation_hash(contract()) :: hash()
def get_contract_creation_hash(contract), do: Contract.get_creation_hash(contract)
@doc """
Returns the address of a contract. If the creation transaction has not been mined yet, `nil` is returned.
"""
@spec get_contract_address(contract()) :: {:ok, address() | nil} | {:error, error()}
def get_contract_address(contract), do: Contract.get_address(contract)
@doc """
Works like `get_contract_creation_address/1` but a exception is raised on error.
"""
@spec get_contract_address!(contract()) :: address() | nil
def get_contract_address!(contract) do
{:ok, address} = get_contract_address(contract)
address
end
######################################################################
## Public helpers
@doc """
String representation of a value given an Ether unit.
## Examples
iex> Etherex.wei_to_string(0)
"0 WEI"
iex> Etherex.wei_to_string(0, :WEI)
"0 WEI"
iex> Etherex.wei_to_string(0, :GWEI)
"0.000000000 GWEI"
iex> Etherex.wei_to_string(0, :PWEI)
"0.000000000000000 PWEI"
iex> Etherex.wei_to_string(0, :ETH)
"0.000000000000000000 ETH"
iex> Etherex.wei_to_string(20000000000)
"20,000,000,000 WEI"
iex> Etherex.wei_to_string(20000000000, :WEI)
"20,000,000,000 WEI"
iex> Etherex.wei_to_string(20000000000, :GWEI)
"20.000000000 GWEI"
iex> Etherex.wei_to_string(20000000000, :PWEI)
"0.000020000000000 PWEI"
iex> Etherex.wei_to_string(20000000000, :ETH)
"0.000000020000000000 ETH"
"""
@spec wei_to_string(quantity(), unit()) :: String.t()
def wei_to_string(wei, currency \\ :WEI) when is_integer(wei) do
Money.new(wei, currency)
|> Money.to_string(symbol_space: true, symbol_on_right: true)
end
@doc """
Decimal representation of a value given an Ether unit.
## Examples
iex> Etherex.wei_to_decimal(0)
Decimal.new("0")
iex> Etherex.wei_to_decimal(0, :WEI)
Decimal.new("0")
iex> Etherex.wei_to_decimal(0, :ETH)
Decimal.new("0E-18")
iex> Etherex.wei_to_decimal(20000000000)
Decimal.new("20000000000")
iex> Etherex.wei_to_decimal(20000000000, :WEI)
Decimal.new("20000000000")
iex> Etherex.wei_to_decimal(20000000000, :GWEI)
Decimal.new("20.000000000")
iex> Etherex.wei_to_decimal(20000000000, :PWEI)
Decimal.new("0.000020000000000")
iex> Etherex.wei_to_decimal(20000000000, :ETH)
Decimal.new("2.0000000000E-8")
"""
@spec wei_to_decimal(quantity(), unit()) :: Decimal.t()
def wei_to_decimal(wei, currency \\ :WEI) when is_integer(wei) do
Money.new(wei, currency)
|> Money.to_decimal()
end
######################################################################
## Helpers
@spec decode_quantities(nil, [String.t()]) :: nil
@spec decode_quantities(map(), [String.t()]) :: map()
defp decode_quantities(value, keys \\ [])
defp decode_quantities(nil, _keys), do: nil
defp decode_quantities(map, keys) when is_map(map) do
Enum.reduce(
keys,
map,
fn k, m ->
Map.update!(
m,
k,
fn
nil ->
nil
v ->
decode_quantity(v)
end
)
end
)
end
@spec decode_transaction(map()) :: map()
defp decode_transaction(transaction) do
transaction
|> decode_quantities([
"blockNumber",
"gas",
"gasPrice",
"transactionIndex",
"v",
"value"
])
end
end