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(), hash()) :: {:ok, [event()] | nil} | {:error, error()} def get_events(contract, hash) do Contract.get_events(contract, 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.0") iex> Etherex.wei_to_decimal(0, :WEI) Decimal.new("0.0") iex> Etherex.wei_to_decimal(0, :ETH) Decimal.new("0.0") iex> Etherex.wei_to_decimal(20000000000) Decimal.new("2E+10") iex> Etherex.wei_to_decimal(20000000000, :WEI) Decimal.new("2E+10") iex> Etherex.wei_to_decimal(20000000000, :GWEI) Decimal.new("20.0") iex> Etherex.wei_to_decimal(20000000000, :PWEI) Decimal.new("0.00002") iex> Etherex.wei_to_decimal(20000000000, :ETH) Decimal.new("2E-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