From 40513f8ac4cac0b638edd18bc51443c16a503d70 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sun, 21 Jan 2018 20:32:29 +0100 Subject: [PATCH] major ongoing progress on queries rewrite --- .formatter.exs | 4 + config/config.exs | 3 +- config/dev.exs | 3 + config/test.exs | 3 + lib/eth.ex | 54 +++-- lib/eth/query.ex | 315 ++++++++++++++++--------- lib/eth/transaction.ex | 101 ++++++-- lib/eth/transaction/parser.ex | 147 +++++++++--- lib/eth/transaction/setter.ex | 80 +++++-- lib/eth/transaction/signer.ex | 112 ++++++--- lib/eth/transaction_queries.ex | 171 ++++++++++++++ lib/eth/utils.ex | 75 +++--- lib/eth/wallet.ex | 21 +- mix.exs | 13 +- mix.lock | 17 +- test/eth/eth.ex | 16 -- test/eth/eth_test.exs | 72 ++++++ test/eth/query_test.exs | 321 ++++++++++++++++++++++---- test/eth/transaction_queries_test.exs | 37 +++ test/eth/transaction_test.exs | 123 ++++++---- test/eth/utils_test.exs | 31 +-- test/eth/wallet_test.exs | 5 +- test/test_helper.exs | 2 + 23 files changed, 1320 insertions(+), 406 deletions(-) create mode 100644 .formatter.exs create mode 100644 config/dev.exs create mode 100644 config/test.exs create mode 100644 lib/eth/transaction_queries.ex delete mode 100644 test/eth/eth.ex create mode 100644 test/eth/eth_test.exs create mode 100644 test/eth/transaction_queries_test.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/config/config.exs b/config/config.exs index adc4b32..aced25f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -27,4 +27,5 @@ use Mix.Config # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # -# import_config "#{Mix.env}.exs" + +import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..3272734 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,3 @@ +use Mix.Config + +config :ethereumex, scheme: "https", host: "localhost", port: 8545 diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..3272734 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,3 @@ +use Mix.Config + +config :ethereumex, scheme: "https", host: "localhost", port: 8545 diff --git a/lib/eth.ex b/lib/eth.ex index eacc190..893c45c 100644 --- a/lib/eth.ex +++ b/lib/eth.ex @@ -1,15 +1,16 @@ defmodule ETH do @moduledoc """ - Documentation for Eth. + Elixir module that provides Ethereum utility functions """ @doc """ - Hello world. + In order to use most of the functions in this library you need to be connected to an ethereum node. + This could be your own self-hosted node running locally or a public proxy. ## Examples - iex> Eth.hello - :world + iex> ETH.block_number + 46080211 """ Application.put_env(:ethereumex, :scheme, Application.get_env(:eth, :scheme, "http")) @@ -17,19 +18,38 @@ defmodule ETH do Application.put_env(:ethereumex, :port, Application.get_env(:eth, :port, 8545)) defdelegate block_number, to: ETH.Query + defdelegate block_number!, to: ETH.Query defdelegate syncing, to: ETH.Query + defdelegate syncing!, to: ETH.Query defdelegate get_accounts, to: ETH.Query + defdelegate get_accounts!, to: ETH.Query defdelegate gas_price, to: ETH.Query + defdelegate gas_price!, to: ETH.Query defdelegate call(call_params), to: ETH.Query + defdelegate call!(call_params), to: ETH.Query + defdelegate get_block, to: ETH.Query defdelegate get_block(identifier), to: ETH.Query - defdelegate get_block_transactions(identifier), to: ETH.Query - defdelegate get_block_transaction_count(identifier), to: ETH.Query - defdelegate get_transaction_from_block(identifier, index), to: ETH.Query + defdelegate get_block!, to: ETH.Query + defdelegate get_block!(identifier), to: ETH.Query + defdelegate get_balance(wallet_or_address), to: ETH.Query defdelegate get_balance(wallet_or_address, denomination), to: ETH.Query - defdelegate get_transaction(transaction_hash), to: ETH.Query - defdelegate get_transaction_receipt(transaction_hash), to: ETH.Query - defdelegate get_transaction_count(wallet_or_address), to: ETH.Query + defdelegate get_balance!(wallet_or_address), to: ETH.Query + defdelegate get_balance!(wallet_or_address, denomination), to: ETH.Query defdelegate estimate_gas(transaction), to: ETH.Query + defdelegate estimate_gas!(transaction), to: ETH.Query + defdelegate estimate_gas(transaction, denomination), to: ETH.Query + defdelegate estimate_gas!(transaction, denomination), to: ETH.Query + + defdelegate get_block_transactions(identifier), to: ETH.TransactionQueries + defdelegate get_block_transactions!(identifier), to: ETH.TransactionQueries + defdelegate get_block_transaction_count(identifier), to: ETH.TransactionQueries + defdelegate get_block_transaction_count!(identifier), to: ETH.TransactionQueries + defdelegate get_transaction_from_block(identifier, index), to: ETH.TransactionQueries + defdelegate get_transaction_from_block!(identifier, index), to: ETH.TransactionQueries + defdelegate get_transaction(transaction_hash), to: ETH.TransactionQueries + defdelegate get_transaction_receipt(transaction_hash), to: ETH.TransactionQueries + defdelegate get_transaction_count(wallet_or_address), to: ETH.TransactionQueries + defdelegate get_transaction_count!(wallet_or_address), to: ETH.TransactionQueries defdelegate parse(data), to: ETH.Transaction.Parser defdelegate to_list(data), to: ETH.Transaction.Parser @@ -41,7 +61,10 @@ defmodule ETH do defdelegate hash_transaction(transaction), to: ETH.Transaction.Signer defdelegate hash_transaction(transaction, include_signature), to: ETH.Transaction.Signer defdelegate hash_transaction_list(transaction_list), to: ETH.Transaction.Signer - defdelegate hash_transaction_list(transaction_list, include_signature), to: ETH.Transaction.Signer + + defdelegate hash_transaction_list(transaction_list, include_signature), + to: ETH.Transaction.Signer + defdelegate sign_transaction(transaction, private_key), to: ETH.Transaction.Signer defdelegate sign_transaction_list(transaction_list, private_key), to: ETH.Transaction.Signer defdelegate decode(rlp_encoded_transaction), to: ETH.Transaction.Signer @@ -49,8 +72,13 @@ defmodule ETH do defdelegate hash(transaction, include_signature), to: ETH.Transaction defdelegate send_transaction(params_or_wallet, private_key_or_params), to: ETH.Transaction - defdelegate send_transaction(sender_wallet, receiver_wallet, value_or_params), to: ETH.Transaction - defdelegate send_transaction(sender_wallet, receiver_wallet, value_or_params, private_key), to: ETH.Transaction + + defdelegate send_transaction(sender_wallet, receiver_wallet, value_or_params), + to: ETH.Transaction + + defdelegate send_transaction(sender_wallet, receiver_wallet, value_or_params, private_key), + to: ETH.Transaction + defdelegate send(signature), to: ETH.Transaction defdelegate get_senders_public_key(transaction_input), to: ETH.Transaction defdelegate get_sender_address(transaction_input), to: ETH.Transaction diff --git a/lib/eth/query.ex b/lib/eth/query.ex index ea2c512..f626fa8 100644 --- a/lib/eth/query.ex +++ b/lib/eth/query.ex @@ -1,139 +1,179 @@ -# TODO: use Macro.underscore defmodule ETH.Query do import ETH.Utils + alias Ethereumex.HttpClient def block_number do - Ethereumex.HttpClient.eth_block_number - |> get_number_result + case HttpClient.eth_block_number() do + {:ok, hex_block_number} -> {:ok, convert_to_number(hex_block_number)} + error -> error + end end - def syncing do - Ethereumex.HttpClient.eth_syncing - |> get_result + def block_number! do + {:ok, hex_block_number} = HttpClient.eth_block_number() + + hex_block_number |> convert_to_number end - def get_accounts do - Ethereumex.HttpClient.eth_accounts - |> get_result + def syncing, do: HttpClient.eth_syncing() + + def syncing! do + {:ok, result} = HttpClient.eth_syncing() + + result + end + + def get_accounts, do: HttpClient.eth_accounts() + + def get_accounts! do + {:ok, accounts} = HttpClient.eth_accounts() + + accounts end def gas_price do - Ethereumex.HttpClient.eth_gas_price() - |> get_number_result + case HttpClient.eth_gas_price() do + {:ok, hex_gas_price} -> {:ok, convert_to_number(hex_gas_price)} + error -> error + end end - def call(call_params) do - Ethereumex.HttpClient.eth_call([call_params]) - |> get_result + def gas_price! do + {:ok, hex_gas_price} = HttpClient.eth_gas_price() + + convert_to_number(hex_gas_price) end - def get_block(block_number) when is_number(block_number) do - Ethereumex.HttpClient.eth_get_block_by_number(["0x" <> Hexate.encode(block_number), true]) - |> get_result # TODO: make camelCased to snake_cased - |> convert_block_details + # TODO: test this one + def call(call_params), do: HttpClient.eth_call(call_params) + + def call!(call_params) do + {:ok, result} = HttpClient.eth_call(call_params) + + result end - def get_block(block_hash) do - Ethereumex.HttpClient.eth_get_block_by_hash([block_hash, true]) - |> get_result - |> convert_block_details + + def get_block do + case HttpClient.eth_get_block_by_number( + "0x" <> Hexate.encode(block_number!()), + true + ) do + {:ok, raw_block_details} -> {:ok, convert_block_details(raw_block_details)} + error -> error + end end - def get_block_transactions(identifier) do - get_block(identifier) - |> Map.get("transactions") + def get_block(block_number) when is_number(block_number) do + case HttpClient.eth_get_block_by_number("0x" <> Hexate.encode(block_number), true) do + {:ok, raw_block_details} -> {:ok, convert_block_details(raw_block_details)} + error -> error + end end - def get_block_transaction_count(block_number) when is_number(block_number) do - Ethereumex.HttpClient.eth_get_block_transaction_count_by_number([block_number]) - |> get_result + def get_block(block_hash) do + case HttpClient.eth_get_block_by_number(block_hash, true) do + {:ok, raw_block_details} -> {:ok, convert_block_details(raw_block_details)} + error -> error + end end - def get_block_transaction_count(block_hash) do - Ethereumex.HttpClient.eth_get_block_transaction_count_by_hash([block_hash]) - |> get_result + + def get_block! do + block_hash = "0x" <> Hexate.encode(block_number!()) + + {:ok, raw_block_details} = HttpClient.eth_get_block_by_number(block_hash, true) + + convert_block_details(raw_block_details) end - def get_transaction_from_block(block_number, index) when is_number(block_number) do - Ethereumex.HttpClient.eth_get_transaction_by_block_number_and_index([block_number, index]) - |> get_result + def get_block!(block_number) when is_number(block_number) do + block_hash = "0x" <> Hexate.encode(block_number) + + {:ok, raw_block_details} = HttpClient.eth_get_block_by_number(block_hash, true) + + convert_block_details(raw_block_details) end - def get_transaction_from_block(block_hash, index) do - Ethereumex.HttpClient.eth_get_transaction_by_block_hash_and_index([block_number, index]) - |> get_result + + def get_block!(block_hash) do + {:ok, raw_block_details} = HttpClient.eth_get_block_by_number(block_hash, true) + + convert_block_details(raw_block_details) end def get_balance(param, denomination \\ :ether) + def get_balance(wallet, denomination) when is_map(wallet) do - Ethereumex.HttpClient.eth_get_balance([wallet.eth_address]) - |> get_number_result - |> convert(denomination) + case HttpClient.eth_get_balance(wallet.eth_address) do + {:ok, hex_balance} -> + balance = + hex_balance + |> convert_to_number + |> convert(denomination) + + {:ok, balance} + + error -> + error + end end + def get_balance(eth_address, denomination) do - Ethereumex.HttpClient.eth_get_balance([eth_address]) - |> get_number_result - |> convert(denomination) - end + case HttpClient.eth_get_balance(eth_address) do + {:ok, hex_balance} -> + balance = + hex_balance + |> convert_to_number + |> convert(denomination) + + {:ok, balance} - def get_transaction(transaction_hash) do - Ethereumex.HttpClient.eth_get_transaction_by_hash([transaction_hash]) - |> get_result - |> convert_transaction_details + error -> + error + end end - def get_transaction_receipt(transaction_hash) do - Ethereumex.HttpClient.eth_get_transaction_receipt([transaction_hash]) - |> get_result - |> Enum.reduce(%{}, fn(tuple, acc) -> - {key, value} = tuple + def get_balance!(param, denomination \\ :ether) - case key do - "transactionHash" -> Map.put(acc, :transaction_hash, value) - "transactionIndex" -> Map.put(acc, :transaction_index, convert_to_number(value)) - "blockHash" -> Map.put(acc, :block_hash, value) - "blockNumber" -> Map.put(acc, :block_number, convert_to_number(value)) - "cumulativeGasUsed" -> Map.put(acc, :cumulative_gas_used, convert_to_number(value)) - "gasUsed" -> Map.put(acc, :gas_used, convert_to_number(value)) - "contractAddress" -> Map.put(acc, :contract_address, value) - "logs" -> - Map.put(acc, :logs, Enum.map(value, fn(log) -> - convert_transaction_log(log) - end)) - "logsBloom" -> Map.put(acc, :logs_bloom, value) - _ -> Map.put(acc, String.to_atom(key), value) - end - end) - end + def get_balance!(wallet, denomination) when is_map(wallet) do + {:ok, hex_balance} = HttpClient.eth_get_balance(wallet.eth_address) - def get_transaction_count(wallet) when is_map(wallet) do - Ethereumex.HttpClient.eth_get_transaction_count([wallet.eth_address]) - |> get_number_result - end - def get_transaction_count(eth_address) do - Ethereumex.HttpClient.eth_get_transaction_count([eth_address]) - |> get_number_result + hex_balance + |> convert_to_number + |> convert(denomination) end - def estimate_gas(transaction \\ %{data: ""}) - def estimate_gas(transaction = %{to: _to, data: _data}) do - Ethereumex.HttpClient.eth_estimate_gas([transaction]) - |> get_number_result + def get_balance!(eth_address, denomination) do + {:ok, hex_balance} = HttpClient.eth_get_balance(eth_address) + + hex_balance + |> convert_to_number + |> convert(denomination) end - defp get_result({:ok, eth_result}), do: Map.get(eth_result, "result") - defp get_result(error), do: raise error + def estimate_gas(transaction \\ %{data: ""}, denomination \\ :wei) + + def estimate_gas(transaction = %{to: _to, data: _data}, denomination) do + case HttpClient.eth_estimate_gas(transaction) do + {:ok, hex_gas_estimate} -> + {:ok, hex_gas_estimate |> convert_to_number |> convert(denomination)} - defp get_number_result(eth_result) do - get_result(eth_result) |> convert_to_number + error -> + error + end end - defp convert_to_number(result) do - result - |> String.slice(2..-1) - |> Hexate.to_integer + def estimate_gas!(transaction \\ %{data: ""}, denomaination \\ :wei) + + def estimate_gas!(transaction = %{to: _to, data: _data}, denomination) do + {:ok, hex_gas_estimate} = HttpClient.eth_estimate_gas(transaction) + + hex_gas_estimate |> convert_to_number |> convert(denomination) end - defp convert_transaction_log(log) do - Enum.reduce(log, %{}, fn(tuple, acc) -> + defp get_result({:ok, eth_result}), do: Map.get(eth_result, "result") + defp get_result(error), do: raise(error) + def convert_transaction_log(log) do + Enum.reduce(log, %{}, fn tuple, acc -> {key, value} = tuple case key do @@ -147,8 +187,9 @@ defmodule ETH.Query do end end) end - defp convert_transaction_details(transaction) do - Enum.reduce(transaction, %{}, fn(tuple, acc) -> + + def convert_transaction_details(transaction) do + Enum.reduce(transaction, %{}, fn tuple, acc -> {key, value} = tuple case key do @@ -167,34 +208,80 @@ defmodule ETH.Query do end) end - defp convert_block_details(result) do + def convert_block_details(result) do result - |> Enum.reduce(%{}, fn(tuple, acc) -> + |> Enum.reduce(%{}, fn tuple, acc -> {key, value} = tuple case key do - "number" -> Map.put(acc, :number, convert_to_number(value)) - "parentHash" -> Map.put(acc, :parent_hash, value) - "mixHash" -> Map.put(acc, :mix_hash, value) - "sha3Uncles" -> Map.put(acc, :sha3_uncles, value) - "logsBloom" -> Map.put(acc, :logs_bloom, value) - "transactionsRoot" -> Map.put(acc, :transactions_root, value) - "stateRoot" -> Map.put(acc, :state_root, value) - "receiptsRoot" -> Map.put(acc, :receipt_root, value) - "extraData" -> Map.put(acc, :extra_data, value) - "size" -> Map.put(acc, :size, convert_to_number(value)) - "sealFields" -> Map.put(acc, :seal_fields, value) - "gasLimit" -> Map.put(acc, :gas_limit, convert_to_number(value)) - "gasUsed" -> Map.put(acc, :gas_used, convert_to_number(value)) - "timestamp" -> Map.put(acc, :timestamp, convert_to_number(value)) - "difficulty" -> Map.put(acc, :difficulty, convert_to_number(value)) - "totalDifficulty" -> Map.put(acc, :total_difficulty, convert_to_number(value)) + "number" -> + Map.put(acc, :number, convert_to_number(value)) + + "parentHash" -> + Map.put(acc, :parent_hash, value) + + "mixHash" -> + Map.put(acc, :mix_hash, value) + + "sha3Uncles" -> + Map.put(acc, :sha3_uncles, value) + + "logsBloom" -> + Map.put(acc, :logs_bloom, value) + + "transactionsRoot" -> + Map.put(acc, :transactions_root, value) + + "stateRoot" -> + Map.put(acc, :state_root, value) + + "receiptsRoot" -> + Map.put(acc, :receipt_root, value) + + "extraData" -> + Map.put(acc, :extra_data, value) + + "size" -> + Map.put(acc, :size, convert_to_number(value)) + + "sealFields" -> + Map.put(acc, :seal_fields, value) + + "gasLimit" -> + Map.put(acc, :gas_limit, convert_to_number(value)) + + "gasUsed" -> + Map.put(acc, :gas_used, convert_to_number(value)) + + "timestamp" -> + Map.put(acc, :timestamp, convert_to_number(value)) + + "difficulty" -> + Map.put(acc, :difficulty, convert_to_number(value)) + + "totalDifficulty" -> + Map.put(acc, :total_difficulty, convert_to_number(value)) + "transactions" -> - Map.put(acc, String.to_atom(key), Enum.map(value, fn(transaction) -> + Map.put( + acc, + String.to_atom(key), + Enum.map(value, fn transaction -> convert_transaction_details(transaction) - end)) - _ -> Map.put(acc, String.to_atom(key), value) + end) + ) + + _ -> + Map.put(acc, String.to_atom(key), value) end end) end + + defp convert_to_number(result) do + result + |> String.slice(2..-1) + |> Hexate.to_integer() + end end + +# TODO: use Macro.underscore diff --git a/lib/eth/transaction.ex b/lib/eth/transaction.ex index e9aa606..4514f2c 100644 --- a/lib/eth/transaction.ex +++ b/lib/eth/transaction.ex @@ -11,17 +11,22 @@ defmodule ETH.Transaction do defdelegate hash_transaction(transaction), to: ETH.Transaction.Signer defdelegate hash_transaction(transaction, include_signature), to: ETH.Transaction.Signer defdelegate hash_transaction_list(transaction_list), to: ETH.Transaction.Signer - defdelegate hash_transaction_list(transaction_list, include_signature), to: ETH.Transaction.Signer + + defdelegate hash_transaction_list(transaction_list, include_signature), + to: ETH.Transaction.Signer + defdelegate sign_transaction(transaction, private_key), to: ETH.Transaction.Signer defdelegate sign_transaction_list(transaction_list, private_key), to: ETH.Transaction.Signer defdelegate decode(rlp_encoded_transaction), to: ETH.Transaction.Signer defdelegate encode(signed_transaction_list), to: ETH.Transaction.Signer def hash(transaction, include_signature \\ true) + def hash(transaction, include_signature) when is_list(transaction) do ETH.Transaction.Signer.hash_transaction_list(transaction, include_signature) end - def hash(transaction=%{}, include_signature) do + + def hash(transaction = %{}, include_signature) do ETH.Transaction.Signer.hash_transaction(transaction, include_signature) end @@ -30,80 +35,127 @@ defmodule ETH.Transaction do |> Map.merge(%{from: wallet.eth_address}) |> to_transaction(wallet.private_key) end - def send_transaction(params, private_key) when is_list(params) do # NOTE: check params.from + + # NOTE: check params.from + def send_transaction(params, private_key) when is_list(params) do params |> to_transaction(private_key) end + def send_transaction(params, private_key) when is_map(params) do params |> to_transaction(private_key) end + def send_transaction(sender_wallet, receiver_wallet, value) when is_number(value) do %{from: sender_wallet.eth_address, to: receiver_wallet.eth_address, value: value} |> to_transaction(sender_wallet.private_key) end + def send_transaction(sender_wallet, receiver_wallet, params) when is_map(params) do params |> Map.merge(%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address}) |> to_transaction(sender_wallet.private_key) end + def send_transaction(sender_wallet, receiver_wallet, params) when is_list(params) do params - |> Keyword.merge([from: sender_wallet.eth_address, to: receiver_wallet.eth_address]) + |> Keyword.merge(from: sender_wallet.eth_address, to: receiver_wallet.eth_address) |> to_transaction(sender_wallet.private_key) end + def send_transaction(sender_wallet, receiver_wallet, value, private_key) when is_number(value) do %{from: sender_wallet.eth_address, to: receiver_wallet.eth_address, value: value} |> to_transaction(private_key) end + def send_transaction(sender_wallet, receiver_wallet, params, private_key) when is_map(params) do params |> Map.merge(%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address}) |> to_transaction(private_key) end + def send_transaction(sender_wallet, receiver_wallet, params, private_key) when is_list(params) do params - |> Keyword.merge([from: sender_wallet.eth_address, to: receiver_wallet.eth_address]) + |> Keyword.merge(from: sender_wallet.eth_address, to: receiver_wallet.eth_address) |> to_transaction(private_key) end def send(signature), do: Ethereumex.HttpClient.eth_send_raw_transaction([signature]) - def get_senders_public_key("0x" <> rlp_encoded_transaction_list) do # NOTE: not tested + # NOTE: not tested + def get_senders_public_key("0x" <> rlp_encoded_transaction_list) do rlp_encoded_transaction_list |> Base.decode16!(case: :mixed) |> to_senders_public_key end + def get_senders_public_key(<>) do encoded_tx |> Base.decode16!(case: :mixed) - |> ExRLP.decode + |> ExRLP.decode() |> to_senders_public_key end - def get_senders_public_key(transaction_list = [ - _nonce, _gas_price, _gas_limit, _to, _value, _data, _v, _r, _s - ]), do: to_senders_public_key(transaction_list) - def get_sender_address("0x" <> rlp_encoded_transaction_list) do # NOTE: not tested + def get_senders_public_key( + transaction_list = [ + _nonce, + _gas_price, + _gas_limit, + _to, + _value, + _data, + _v, + _r, + _s + ] + ), + do: to_senders_public_key(transaction_list) + + # NOTE: not tested + def get_sender_address("0x" <> rlp_encoded_transaction_list) do rlp_encoded_transaction_list - |> ExRLP.decode + |> ExRLP.decode() |> get_senders_public_key |> get_address end + def get_sender_address(<>) do encoded_tx |> Base.decode16!(case: :mixed) - |> ExRLP.decode + |> ExRLP.decode() |> to_senders_public_key |> get_address end - def get_sender_address(transaction_list = [ - _nonce, _gas_price, _gas_limit, _to, _value, _data, _v, _r, _s - ]), do: get_senders_public_key(transaction_list) |> get_address - defp to_senders_public_key(transaction_list = [ - _nonce, _gas_price, _gas_limit, _to, _value, _data, v, r, s - ]) do + def get_sender_address( + transaction_list = [ + _nonce, + _gas_price, + _gas_limit, + _to, + _value, + _data, + _v, + _r, + _s + ] + ), + do: get_senders_public_key(transaction_list) |> get_address + + defp to_senders_public_key( + transaction_list = [ + _nonce, + _gas_price, + _gas_limit, + _to, + _value, + _data, + v, + r, + s + ] + ) do message_hash = hash_transaction_list(transaction_list, false) chain_id = get_chain_id(v, Enum.at(transaction_list, 9)) v_int = buffer_to_int(v) @@ -112,16 +164,20 @@ defmodule ETH.Transaction do signature = r <> s recovery_id = target_v - 27 - {:ok, public_key} = :libsecp256k1.ecdsa_recover_compact(message_hash, signature, :uncompressed, recovery_id) + {:ok, public_key} = + :libsecp256k1.ecdsa_recover_compact(message_hash, signature, :uncompressed, recovery_id) + public_key end defp to_transaction(params, private_key) do target_params = set_default_from(params, private_key) - result = target_params + + result = + target_params |> set |> sign_transaction(private_key) - |> Base.encode16 + |> Base.encode16() |> send case result do @@ -133,6 +189,7 @@ defmodule ETH.Transaction do defp set_default_from(params, private_key) when is_list(params) do Keyword.get(params, :from, get_address(private_key)) end + defp set_default_from(params, private_key) when is_map(params) do Map.get(params, :from, get_address(private_key)) end diff --git a/lib/eth/transaction/parser.ex b/lib/eth/transaction/parser.ex index 3a489be..ad8ace5 100644 --- a/lib/eth/transaction/parser.ex +++ b/lib/eth/transaction/parser.ex @@ -2,76 +2,151 @@ defmodule ETH.Transaction.Parser do import ETH.Utils def parse("0x" <> encoded_transaction_rlp) do - [nonce, gas_price, gas_limit, to, value, data, v, r, s] = encoded_transaction_rlp + [nonce, gas_price, gas_limit, to, value, data, v, r, s] = + encoded_transaction_rlp |> Base.decode16!(case: :mixed) - |> ExRLP.decode + |> ExRLP.decode() %{ - nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, - value: value, data: data, v: v, r: r, s: s + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + to: to, + value: value, + data: data, + v: v, + r: r, + s: s } end + def parse(<>) do - [nonce, gas_price, gas_limit, to, value, data, v, r, s] = transaction_rlp - |> ExRLP.decode + [nonce, gas_price, gas_limit, to, value, data, v, r, s] = + transaction_rlp + |> ExRLP.decode() %{ - nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, - value: value, data: data, v: v, r: r, s: s + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + to: to, + value: value, + data: data, + v: v, + r: r, + s: s } end + def parse(_transaction_list = [nonce, gas_price, gas_limit, to, value, data]) do %{ - nonce: to_buffer(nonce), gas_price: to_buffer(gas_price), gas_limit: to_buffer(gas_limit), - to: to_buffer(to), value: to_buffer(value), data: to_buffer(data) + nonce: to_buffer(nonce), + gas_price: to_buffer(gas_price), + gas_limit: to_buffer(gas_limit), + to: to_buffer(to), + value: to_buffer(value), + data: to_buffer(data) } end + def parse(_transaction_list = [nonce, gas_price, gas_limit, to, value, data, v, r, s]) do %{ - nonce: to_buffer(nonce), gas_price: to_buffer(gas_price), gas_limit: to_buffer(gas_limit), - to: to_buffer(to), value: to_buffer(value), data: to_buffer(data), v: to_buffer(v), - r: to_buffer(r), s: to_buffer(s) + nonce: to_buffer(nonce), + gas_price: to_buffer(gas_price), + gas_limit: to_buffer(gas_limit), + to: to_buffer(to), + value: to_buffer(value), + data: to_buffer(data), + v: to_buffer(v), + r: to_buffer(r), + s: to_buffer(s) } end - def parse(_transaction = %{ - nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data - }) do + + def parse( + _transaction = %{ + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + to: to, + value: value, + data: data + } + ) do %{ - nonce: to_buffer(nonce), gas_price: to_buffer(gas_price), gas_limit: to_buffer(gas_limit), - to: to_buffer(to), value: to_buffer(value), data: to_buffer(data) + nonce: to_buffer(nonce), + gas_price: to_buffer(gas_price), + gas_limit: to_buffer(gas_limit), + to: to_buffer(to), + value: to_buffer(value), + data: to_buffer(data) } end - def parse(_transaction = %{ - nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data, - v: v, r: r, s: s - }) do + + def parse( + _transaction = %{ + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + to: to, + value: value, + data: data, + v: v, + r: r, + s: s + } + ) do %{ - nonce: to_buffer(nonce), gas_price: to_buffer(gas_price), gas_limit: to_buffer(gas_limit), - to: to_buffer(to), value: to_buffer(value), data: to_buffer(data), v: to_buffer(v), - r: to_buffer(r), s: to_buffer(s) + nonce: to_buffer(nonce), + gas_price: to_buffer(gas_price), + gas_limit: to_buffer(gas_limit), + to: to_buffer(to), + value: to_buffer(value), + data: to_buffer(data), + v: to_buffer(v), + r: to_buffer(r), + s: to_buffer(s) } end def to_list("0x" <> encoded_transaction_rlp) do - encoded_transaction_rlp |> Base.decode16!(case: :mixed) |> ExRLP.decode + encoded_transaction_rlp |> Base.decode16!(case: :mixed) |> ExRLP.decode() end - def to_list(<>), do: transaction_rlp |> ExRLP.decode - def to_list(_transaction = %{ - nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data, - v: v, r: r, s: s - }) do + + def to_list(<>), do: transaction_rlp |> ExRLP.decode() + + def to_list( + _transaction = %{ + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + to: to, + value: value, + data: data, + v: v, + r: r, + s: s + } + ) do [nonce, gas_price, gas_limit, to, value, data, v, r, s] - |> Enum.map(fn(value) -> to_buffer(value) end) + |> Enum.map(fn value -> to_buffer(value) end) end - def to_list(transaction = %{ - nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, value: value, data: data - }) do + + def to_list( + transaction = %{ + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + value: value, + data: data + } + ) do to = Map.get(transaction, :to, "") v = Map.get(transaction, :v, <<28>>) r = Map.get(transaction, :r, "") s = Map.get(transaction, :s, "") [nonce, gas_price, gas_limit, to, value, data, v, r, s] - |> Enum.map(fn(value) -> to_buffer(value) end) + |> Enum.map(fn value -> to_buffer(value) end) end end diff --git a/lib/eth/transaction/setter.ex b/lib/eth/transaction/setter.ex index 80abfbc..ed896dc 100644 --- a/lib/eth/transaction/setter.ex +++ b/lib/eth/transaction/setter.ex @@ -6,39 +6,51 @@ defmodule ETH.Transaction.Setter do |> set_params_from_list |> parse end + def set(params) when is_map(params) do params |> set_params_from_map |> parse end + def set(wallet, params) do - result = cond do - is_map(params) -> - target_params = params |> Map.merge(%{from: wallet.eth_address}) - set_params_from_map(target_params) - is_list(params) -> - target_params = params |> Keyword.merge([from: wallet.eth_address]) - set_params_from_list(target_params) - end + result = + cond do + is_map(params) -> + target_params = params |> Map.merge(%{from: wallet.eth_address}) + set_params_from_map(target_params) + + is_list(params) -> + target_params = params |> Keyword.merge(from: wallet.eth_address) + set_params_from_list(target_params) + end parse(result) end + def set(sender_wallet, receiver_wallet, value) when is_number(value) do %{from: sender_wallet.eth_address, to: receiver_wallet.eth_address, value: value} |> set_params_from_map |> parse end + def set(sender_wallet, receiver_wallet, params) do - result = cond do - is_map(params) -> - target_params = params - |> Map.merge(%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address}) - set_params_from_map(target_params) - is_list(params) -> - target_params = params - |> Keyword.merge([from: sender_wallet.eth_address, to: receiver_wallet.eth_address]) - set_params_from_list(target_params) - end + result = + cond do + is_map(params) -> + target_params = + params + |> Map.merge(%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address}) + + set_params_from_map(target_params) + + is_list(params) -> + target_params = + params + |> Keyword.merge(from: sender_wallet.eth_address, to: receiver_wallet.eth_address) + + set_params_from_list(target_params) + end parse(result) end @@ -50,9 +62,19 @@ defmodule ETH.Transaction.Setter do data = Keyword.get(params, :data, "") nonce = Keyword.get(params, :nonce, ETH.Query.get_transaction_count(params[:from])) chain_id = Keyword.get(params, :chain_id, 3) - gas_limit = Keyword.get(params, :gas_limit, ETH.Query.estimate_gas(%{ - to: to, value: value, data: data, nonce: nonce, chain_id: chain_id - })) + + gas_limit = + Keyword.get( + params, + :gas_limit, + ETH.Query.estimate_gas(%{ + to: to, + value: value, + data: data, + nonce: nonce, + chain_id: chain_id + }) + ) %{nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data} end @@ -64,9 +86,19 @@ defmodule ETH.Transaction.Setter do data = Map.get(params, :data, "") nonce = Map.get(params, :nonce, ETH.Query.get_transaction_count(params.from)) chain_id = Map.get(params, :chain_id, 3) - gas_limit = Map.get(params, :gas_limit, ETH.Query.estimate_gas(%{ - to: to, value: value, data: data, nonce: nonce, chain_id: chain_id - })) + + gas_limit = + Map.get( + params, + :gas_limit, + ETH.Query.estimate_gas(%{ + to: to, + value: value, + data: data, + nonce: nonce, + chain_id: chain_id + }) + ) %{nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data} end diff --git a/lib/eth/transaction/signer.ex b/lib/eth/transaction/signer.ex index b29da88..495aa04 100644 --- a/lib/eth/transaction/signer.ex +++ b/lib/eth/transaction/signer.ex @@ -4,37 +4,51 @@ defmodule ETH.Transaction.Signer do alias ETH.Transaction.Parser, as: TransactionParser def hash_transaction(transaction, include_signature \\ true) - def hash_transaction(transaction = %{ - to: _to, value: _value, data: _data, gas_price: _gas_price, gas_limit: _gas_limit, - nonce: _nonce - }, include_signature) do + + def hash_transaction( + transaction = %{ + to: _to, + value: _value, + data: _data, + gas_price: _gas_price, + gas_limit: _gas_limit, + nonce: _nonce + }, + include_signature + ) do chain_id = get_chain_id(Map.get(transaction, :v, <<28>>), Map.get(transaction, :chain_id)) transaction |> Map.delete(:chain_id) - |> TransactionParser.to_list + |> TransactionParser.to_list() |> List.insert_at(-1, chain_id) |> hash_transaction_list(include_signature) end + # NOTE: usage is generally internal def hash_transaction_list( - transaction_list, include_signature \\ true - ) when is_list(transaction_list) do # NOTE: usage is generally internal - target_list = case include_signature do - true -> transaction_list - false -> - # EIP155 spec: - # when computing the hash of a transaction for purposes of signing or recovering, - # instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data), - # hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0 - list = Enum.take(transaction_list, 6) - v = Enum.at(transaction_list, 6) || <<28>> - chain_id = get_chain_id(v, Enum.at(transaction_list, 9)) - if chain_id > 0, do: list ++ [chain_id, 0, 0], else: list - end + transaction_list, + include_signature \\ true + ) + when is_list(transaction_list) do + target_list = + case include_signature do + true -> + transaction_list + + false -> + # EIP155 spec: + # when computing the hash of a transaction for purposes of signing or recovering, + # instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data), + # hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0 + list = Enum.take(transaction_list, 6) + v = Enum.at(transaction_list, 6) || <<28>> + chain_id = get_chain_id(v, Enum.at(transaction_list, 9)) + if chain_id > 0, do: list ++ [chain_id, 0, 0], else: list + end target_list - |> ExRLP.encode + |> ExRLP.encode() |> keccak256 end @@ -46,32 +60,66 @@ defmodule ETH.Transaction.Signer do def sign_transaction(transaction, private_key) when is_map(transaction) do transaction - |> ETH.Transaction.to_list + |> ETH.Transaction.to_list() |> sign_transaction_list(private_key) - |> ExRLP.encode + |> ExRLP.encode() end - def sign_transaction_list(transaction_list = [ - _nonce, _gas_price, _gas_limit, _to, _value, _data, _v, _r, _s - ], << private_key :: binary-size(32) >>) do + def sign_transaction_list( + transaction_list = [ + _nonce, + _gas_price, + _gas_limit, + _to, + _value, + _data, + _v, + _r, + _s + ], + <> + ) do to_signed_transaction_list(transaction_list, private_key) end - def sign_transaction_list(transaction_list = [ - _nonce, _gas_price, _gas_limit, _to, _value, _data, _v, _r, _s - ], << encoded_private_key :: binary-size(64) >>) do + + def sign_transaction_list( + transaction_list = [ + _nonce, + _gas_price, + _gas_limit, + _to, + _value, + _data, + _v, + _r, + _s + ], + <> + ) do decoded_private_key = Base.decode16!(encoded_private_key, case: :mixed) to_signed_transaction_list(transaction_list, decoded_private_key) end - defp to_signed_transaction_list(transaction_list = [ - nonce, gas_price, gas_limit, to, value, data, v, _r, _s - ], << private_key :: binary-size(32) >>) do + defp to_signed_transaction_list( + transaction_list = [ + nonce, + gas_price, + gas_limit, + to, + value, + data, + v, + _r, + _s + ], + <> + ) do chain_id = get_chain_id(v, Enum.at(transaction_list, 9)) message_hash = hash_transaction_list(transaction_list, false) [signature: signature, recovery: recovery] = secp256k1_signature(message_hash, private_key) - << sig_r :: binary-size(32) >> <> << sig_s :: binary-size(32) >> = signature + <> <> <> = signature initial_v = recovery + 27 sig_v = if chain_id > 0, do: initial_v + (chain_id * 2 + 8), else: initial_v diff --git a/lib/eth/transaction_queries.ex b/lib/eth/transaction_queries.ex new file mode 100644 index 0000000..497ac33 --- /dev/null +++ b/lib/eth/transaction_queries.ex @@ -0,0 +1,171 @@ +defmodule ETH.TransactionQueries do + import ETH.Utils + import ETH.Query + + alias Ethereumex.HttpClient + + def get_block_transactions(identifier) do + case get_block(identifier) do + {:ok, block} -> + {:ok, + block |> Map.get("transactions") + |> Enum.map(fn transaction -> + convert_transaction_details(transaction) + end)} + + error -> + error + end + end + + def get_block_transactions!(identifier) do + get_block!(identifier) |> Map.get("transactions") + |> Enum.map(fn transaction -> + convert_transaction_details(transaction) + end) + end + + def get_block_transaction_count(block_number) when is_number(block_number) do + case HttpClient.eth_get_block_transaction_count_by_number(block_number) do + {:ok, transaction_count} -> {:ok, transaction_count} + error -> error + end + end + + def get_block_transaction_count(block_hash) do + case HttpClient.eth_get_block_transaction_count_by_hash(block_hash) do + {:ok, transaction_count} -> {:ok, transaction_count} + error -> error + end + end + + def get_block_transaction_count!(block_number) when is_number(block_number) do + {:ok, transaction_count} = HttpClient.eth_get_block_transaction_count_by_number(block_number) + + transaction_count + end + + def get_block_transaction_count!(block_hash) do + {:ok, transaction_count} = HttpClient.eth_get_block_transaction_count_by_hash(block_hash) + + transaction_count + end + + def get_transaction_from_block(block_number, index) when is_number(block_number) do + case HttpClient.eth_get_transaction_by_block_number_and_index(block_number, index) do + {:ok, transaction} -> {:ok, transaction} + error -> error + end + end + + def get_transaction_from_block(block_hash, index) do + case HttpClient.eth_get_transaction_by_block_hash_and_index(block_number, index) do + {:ok, transaction} -> {:ok, transaction} + error -> error + end + end + + def get_transaction_from_block!(block_number, index) when is_number(block_number) do + {:ok, transaction} = + HttpClient.eth_get_transaction_by_block_number_and_index(block_number, index) + + transaction + end + + def get_transaction_from_block!(block_hash, index) do + {:ok, transaction} = + HttpClient.eth_get_transaction_by_block_hash_and_index(block_number, index) + + transaction + end + + def get_transaction(transaction_hash) do + case HttpClient.eth_get_transaction_by_hash(transaction_hash) do + {:ok, raw_transaction} -> {:ok, convert_transaction_details(raw_transaction)} + error -> error + end + end + + def get_transaction!(transaction_hash) do + {:ok, raw_transaction} = HttpClient.eth_get_transaction_by_hash(transaction_hash) + + convert_transaction_details(raw_transaction) + end + + def get_transaction_receipt(transaction_hash) do + HttpClient.eth_get_transaction_receipt([transaction_hash]) + |> Enum.reduce(%{}, fn tuple, acc -> + {key, value} = tuple + + case key do + "transactionHash" -> + Map.put(acc, :transaction_hash, value) + + "transactionIndex" -> + Map.put(acc, :transaction_index, convert_to_number(value)) + + "blockHash" -> + Map.put(acc, :block_hash, value) + + "blockNumber" -> + Map.put(acc, :block_number, convert_to_number(value)) + + "cumulativeGasUsed" -> + Map.put(acc, :cumulative_gas_used, convert_to_number(value)) + + "gasUsed" -> + Map.put(acc, :gas_used, convert_to_number(value)) + + "contractAddress" -> + Map.put(acc, :contract_address, value) + + "logs" -> + Map.put( + acc, + :logs, + Enum.map(value, fn log -> + convert_transaction_log(log) + end) + ) + + "logsBloom" -> + Map.put(acc, :logs_bloom, value) + + _ -> + Map.put(acc, String.to_atom(key), value) + end + end) + end + + def get_transaction_count(wallet) when is_map(wallet) do + case HttpClient.eth_get_transaction_count(wallet.eth_address) do + {:ok, hex_transaction_count} -> {:ok, convert_to_number(hex_transaction_count)} + error -> error + end + end + + def get_transaction_count(eth_address) do + case HttpClient.eth_get_transaction_count(eth_address) do + {:ok, hex_transaction_count} -> {:ok, convert_to_number(hex_transaction_count)} + error -> error + end + end + + def get_transaction_count!(wallet) when is_map(wallet) do + {:ok, hex_transaction_count} = HttpClient.eth_get_transaction_count(wallet.eth_address) + + convert_to_number(hex_transaction_count) + end + + def get_transaction_count!(eth_address) do + {:ok, hex_transaction_count} = HttpClient.eth_get_transaction_count(eth_address) + + convert_to_number(hex_transaction_count) + end + + defp convert_to_number(result) do + result + |> String.slice(2..-1) + |> Hexate.to_integer() + end +end diff --git a/lib/eth/utils.ex b/lib/eth/utils.ex index f73ff97..d767848 100644 --- a/lib/eth/utils.ex +++ b/lib/eth/utils.ex @@ -1,58 +1,67 @@ defmodule ETH.Utils do - def get_private_key, do: :crypto.strong_rand_bytes(32) - def get_public_key(<< private_key :: binary-size(32) >>) do + def get_public_key(<>) do {:ok, public_key} = :libsecp256k1.ec_pubkey_create(private_key, :uncompressed) public_key end - def get_public_key(<< encoded_private_key :: binary-size(64) >>) do + + def get_public_key(<>) do private_key = Base.decode16!(encoded_private_key, case: :mixed) {:ok, public_key} = :libsecp256k1.ec_pubkey_create(private_key, :uncompressed) public_key end - def get_address(<< private_key :: binary-size(32) >>) do - << 4 :: size(8), key :: binary-size(64) >> = private_key |> get_public_key() - << _ :: binary-size(12), eth_address :: binary-size(20) >> = keccak256(key) + def get_address(<>) do + <<4::size(8), key::binary-size(64)>> = private_key |> get_public_key() + <<_::binary-size(12), eth_address::binary-size(20)>> = keccak256(key) "0x#{Base.encode16(eth_address)}" end - def get_address(<< encoded_private_key :: binary-size(64) >>) do + + def get_address(<>) do public_key = Base.decode16!(encoded_private_key, case: :mixed) |> get_public_key() - << 4 :: size(8), key :: binary-size(64) >> = public_key - << _ :: binary-size(12), eth_address :: binary-size(20) >> = keccak256(key) + <<4::size(8), key::binary-size(64)>> = public_key + <<_::binary-size(12), eth_address::binary-size(20)>> = keccak256(key) "0x#{Base.encode16(eth_address)}" end - def get_address(<< 4 :: size(8), key :: binary-size(64) >>) do - << _ :: binary-size(12), eth_address :: binary-size(20) >> = keccak256(key) + + def get_address(<<4::size(8), key::binary-size(64)>>) do + <<_::binary-size(12), eth_address::binary-size(20)>> = keccak256(key) "0x#{Base.encode16(eth_address)}" end - def get_address(<< encoded_public_key :: binary-size(130) >>) do - << 4 :: size(8), key :: binary-size(64) >> = Base.decode16!(encoded_public_key, case: :mixed) - << _ :: binary-size(12), eth_address :: binary-size(20) >> = keccak256(key) + + def get_address(<>) do + <<4::size(8), key::binary-size(64)>> = Base.decode16!(encoded_public_key, case: :mixed) + <<_::binary-size(12), eth_address::binary-size(20)>> = keccak256(key) "0x#{Base.encode16(eth_address)}" end - def convert(number, denomination \\ :ether) do # NOTE: not tested area: - denom = [ - wei: 1, - kwei: 1000, - mwei: 1000000, - gwei: 1000000000, - shannon: 1000000000, - nano: 1000000000, - szabo: 1000000000000, - micro: 1000000000000, - finney: 1000000000000000, - milli: 1000000000000000, - ether: 1000000000000000000, - ] |> List.keyfind(denomination, 0) |> elem(1) + # NOTE: not tested area: + def convert(number, denomination \\ :ether) do + denom = + [ + wei: 1, + kwei: 1000, + mwei: 1_000_000, + gwei: 1_000_000_000, + shannon: 1_000_000_000, + nano: 1_000_000_000, + szabo: 1_000_000_000_000, + micro: 1_000_000_000_000, + finney: 1_000_000_000_000_000, + milli: 1_000_000_000_000_000, + ether: 1_000_000_000_000_000_000 + ] + |> List.keyfind(denomination, 0) + |> elem(1) number / denom end def secp256k1_signature(hash, private_key) do - {:ok, signature, recovery} = :libsecp256k1.ecdsa_sign_compact(hash, private_key, :default, <<>>) + {:ok, signature, recovery} = + :libsecp256k1.ecdsa_sign_compact(hash, private_key, :default, <<>>) + [signature: signature, recovery: recovery] end @@ -62,20 +71,25 @@ defmodule ETH.Utils do def to_buffer(nil), do: "" def to_buffer(0), do: "" + def to_buffer(data) when is_number(data) do data |> Integer.to_string(16) |> pad_to_even |> Base.decode16!(case: :mixed) end + def to_buffer("0x00"), do: "" + def to_buffer("0x" <> data) do padded_data = pad_to_even(data) + case Base.decode16(padded_data, case: :mixed) do {:ok, decoded_binary} -> decoded_binary _ -> data end end + def to_buffer(data), do: data # NOTE: to_buffer else if (v === null || v === undefined) { v = Buffer.allocUnsafe(0) } @@ -91,7 +105,7 @@ defmodule ETH.Utils do def get_chain_id(v, chain_id \\ nil) do computed_chain_id = compute_chain_id(v) - if computed_chain_id == 0, do: (chain_id || 0), else: computed_chain_id + if computed_chain_id == 0, do: chain_id || 0, else: computed_chain_id end defp compute_chain_id("0x" <> v) do @@ -99,6 +113,7 @@ defmodule ETH.Utils do chain_id = Float.floor((sig_v - 35) / 2) if chain_id < 0, do: 0, else: Kernel.trunc(chain_id) end + defp compute_chain_id(v) do sig_v = buffer_to_int(v) chain_id = Float.floor((sig_v - 35) / 2) diff --git a/lib/eth/wallet.ex b/lib/eth/wallet.ex index fc364a1..fd1b470 100644 --- a/lib/eth/wallet.ex +++ b/lib/eth/wallet.ex @@ -2,19 +2,30 @@ defmodule ETH.Wallet do import ETH.Utils def create(private_key \\ :crypto.strong_rand_bytes(32)) - def create(<< encoded_private_key :: binary-size(64) >>) do + + def create(<>) do public_key = get_public_key(encoded_private_key) eth_address = get_address(public_key) - %{private_key: encoded_private_key, public_key: Base.encode16(public_key), - eth_address: eth_address} + %{ + private_key: encoded_private_key, + public_key: Base.encode16(public_key), + eth_address: eth_address, + mnemonic_phrase: Mnemonic.entropy_to_mnemonic(encoded_private_key) + } end + def create(private_key) do + encoded_private_key = Base.encode16(private_key) public_key = get_public_key(private_key) eth_address = get_address(public_key) - %{private_key: Base.encode16(private_key), public_key: Base.encode16(public_key), - eth_address: eth_address} + %{ + private_key: encoded_private_key, + public_key: Base.encode16(public_key), + eth_address: eth_address, + mnemonic_phrase: Mnemonic.entropy_to_mnemonic(encoded_private_key) + } end def parse(private_key), do: create(private_key) diff --git a/mix.exs b/mix.exs index 6ce2d9f..4125453 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Eth.Mixfile do app: :eth, version: "0.2.2", elixir: "~> 1.5", - start_permanent: Mix.env == :prod, + start_permanent: Mix.env() == :prod, deps: deps(), package: package(), description: description() @@ -23,21 +23,22 @@ defmodule Eth.Mixfile do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ethereumex, "~> 0.1.1"}, + {:ethereumex, "~> 0.2.1"}, {:ex_rlp, "~> 0.2.1"}, {:ex_doc, ">= 0.0.0", only: :dev}, + {:dialyxir, "~> 0.5.1", only: [:dev], runtime: false}, {:hexate, "~> 0.6.1"}, {:keccakf1600, "~> 2.0", hex: :keccakf1600_orig}, - {:mnemonic, "~> 0.2.0"}, + {:mnemonic, "~> 0.2.1"}, {:poison, "~> 3.1"}, {:libsecp256k1, "~> 0.1.3"} ] end defp description do - """ - Ethereum utilities for Elixir. - """ + """ + Ethereum utilities for Elixir. + """ end def package do diff --git a/mix.lock b/mix.lock index da5d599..a9611e3 100644 --- a/mix.lock +++ b/mix.lock @@ -1,20 +1,23 @@ -%{"certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], []}, +%{ + "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], []}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []}, "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []}, - "ethereumex": {:hex, :ethereumex, "0.1.1", "9b308ab4d5f13e784165f977c1117ee3ac8964f5897f67bc7356959569ba9a8b", [:mix], [{:httpoison, "~> 0.11.1", [hex: :httpoison, optional: false]}, {:poison, "~> 3.1.0", [hex: :poison, optional: false]}]}, + "ethereumex": {:hex, :ethereumex, "0.2.1", "7d7156bd933517bf73c89ebc40a3e3cb1cc186318cef4250bd9ce1a88f082a10", [:mix], [{:httpoison, "~> 1.0.0", [hex: :httpoison, optional: false]}, {:poison, "~> 3.1.0", [hex: :poison, optional: false]}]}, "ex_doc": {:hex, :ex_doc, "0.16.3", "cd2a4cfe5d26e37502d3ec776702c72efa1adfa24ed9ce723bb565f4c30bd31a", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, "ex_rlp": {:hex, :ex_rlp, "0.2.1", "bd320900d6316cdfe01d365d4bda22eb2f39b359798daeeffd3bd1ca7ba958ec", [:mix], []}, - "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, optional: false]}, {:idna, "5.0.2", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, + "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, optional: false]}, {:idna, "5.1.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, "hex_prefix": {:hex, :hex_prefix, "0.1.0", "e96b5cbb6ad8493196ce193726240023f5ce0ae0753118a19a5b43e2db0267ca", [:mix], []}, "hexate": {:hex, :hexate, "0.6.1", "1cea42e462c1daa32223127d4752e71016c3d933d492b9bb7fa4709a4a0fd50d", [:mix], []}, - "httpoison": {:hex, :httpoison, "0.11.2", "9e59f17a473ef6948f63c51db07320477bad8ba88cf1df60a3eee01150306665", [:mix], [{:hackney, "~> 1.8.0", [hex: :hackney, optional: false]}]}, - "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, optional: false]}]}, + "httpoison": {:hex, :httpoison, "1.0.0", "1f02f827148d945d40b24f0b0a89afe40bfe037171a6cf70f2486976d86921cd", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, optional: false]}]}, + "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, optional: false]}]}, "keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], []}, "libsecp256k1": {:hex, :libsecp256k1, "0.1.3", "57468b986af7c9633527875f71c7ca08bf4150b07b38a60d5bd48fba299ff6c1", [:rebar3], []}, "libsecp256k1_nif": {:hex, :libsecp256k1_nif, "0.0.3", "3d30ec8fb6a7032c4a3b75cb36ed509f397d33c5e44c53c20b800c1b07f5ab2d", [:rebar3], []}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "mnemonic": {:hex, :mnemonic, "0.2.0", "676a1bebe854c390e04d0030b66225d10a6b0fac33b427a3f3b9b17c2ee5e715", [:mix], []}, + "mnemonic": {:hex, :mnemonic, "0.2.1", "dbdb0a835122219068ca69a64e6d1d0eb4be388f92609b2bc4910cf6a236f316", [:mix], []}, "nif_libsecp256k1": {:hex, :nif_libsecp256k1, "0.0.2", "7548eabb7d04c09c29814f9c8aa8542d7ff2d9b972a8e3c611faef21a24616da", [:rebar3], []}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], []}} + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], []}, +} diff --git a/test/eth/eth.ex b/test/eth/eth.ex deleted file mode 100644 index 97a0f03..0000000 --- a/test/eth/eth.ex +++ /dev/null @@ -1,16 +0,0 @@ -# elliptic curve cryptography library for signing transactions in Ethereum -defmodule ETH do - @moduledoc """ - The essential Elixir library for Ethereum blockchain. - """ - - @doc """ - Hello world. - - ## Examples - - iex> Eth.hello - :world - - """ -end diff --git a/test/eth/eth_test.exs b/test/eth/eth_test.exs new file mode 100644 index 0000000..2bad2e2 --- /dev/null +++ b/test/eth/eth_test.exs @@ -0,0 +1,72 @@ +defmodule ETH.Test do + use ExUnit.Case + + test "public api holds all the methods" do + assert ETH.__info__(:functions) == [ + block_number: 0, + block_number!: 0, + buffer_to_int: 1, + call: 1, + call!: 1, + convert: 2, + decode: 1, + decode16: 1, + encode: 1, + encode16: 1, + estimate_gas: 1, + estimate_gas: 2, + estimate_gas!: 1, + estimate_gas!: 2, + gas_price: 0, + gas_price!: 0, + get_accounts: 0, + get_accounts!: 0, + get_address: 1, + get_balance: 1, + get_balance: 2, + get_balance!: 1, + get_balance!: 2, + get_block: 0, + get_block: 1, + get_block!: 0, + get_block!: 1, + get_block_transaction_count: 1, + get_block_transaction_count!: 1, + get_block_transactions: 1, + get_block_transactions!: 1, + get_chain_id: 2, + get_private_key: 0, + get_public_key: 1, + get_sender_address: 1, + get_senders_public_key: 1, + get_transaction: 1, + get_transaction_count: 1, + get_transaction_count!: 1, + get_transaction_from_block: 2, + get_transaction_from_block!: 2, + get_transaction_receipt: 1, + hash: 2, + hash_transaction: 1, + hash_transaction: 2, + hash_transaction_list: 1, + hash_transaction_list: 2, + keccak256: 1, + pad_to_even: 1, + parse: 1, + secp256k1_signature: 2, + send: 1, + send_transaction: 2, + send_transaction: 3, + send_transaction: 4, + set: 1, + set: 2, + set: 3, + sign_transaction: 2, + sign_transaction_list: 2, + syncing: 0, + syncing!: 0, + to_buffer: 1, + to_list: 1 + ] + end +end diff --git a/test/eth/query_test.exs b/test/eth/query_test.exs index 3ce3878..376aa4e 100644 --- a/test/eth/query_test.exs +++ b/test/eth/query_test.exs @@ -1,70 +1,313 @@ # TODO: write tests for get_transaction\1 and get_transaction_receipt\1 -defmodule ETH.QueryTest do +# TODO: ETH.call +defmodule ETH.Query.Test do use ExUnit.Case + @first_client_account_private_key "a160512c1dc5c33eff6ef89aae083108dcdcabdbe463481949d327fc2ac6ac48" + test "block_number/0 works" do - block_number = ETH.Query.block_number + result = ETH.block_number() + + assert result |> elem(0) == :ok + assert result |> elem(1) |> is_integer + end - assert is_integer(block_number) + test "block_number!/0 works" do + assert is_integer(ETH.block_number!()) end test "syncing/0 works" do - assert ETH.Query.syncing == false + result = ETH.syncing() + + assert result |> elem(0) == :ok + assert result |> elem(1) == false + end + + test "syncing!/0 works" do + assert ETH.syncing!() == false end test "get_accounts/0 works" do - accounts = ETH.Query.get_accounts + result = ETH.get_accounts() + + assert result |> elem(0) == :ok - assert accounts + result + |> elem(1) + |> Enum.each(fn x -> + assert String.length(x) == 42 + assert String.slice(x, 0..1) == "0x" + end) + end - accounts |> Enum.each(fn(x) -> + test "get_accounts!/0 works" do + ETH.get_accounts!() + |> Enum.each(fn x -> assert String.length(x) == 42 assert String.slice(x, 0..1) == "0x" end) end + test "gas_price/0 works" do + result = ETH.gas_price() + + assert result |> elem(0) == :ok + assert result |> elem(1) > 0 + end + + test "gas_price!/0 works" do + assert ETH.gas_price!() > 0 + end + + # TODO: ETH.call + + test "get_block/0 works" do + result = ETH.get_block() + current_block = result |> elem(1) + + assert result |> elem(0) == :ok + assert current_block.number == ETH.block_number!() + + assert Map.keys(current_block) == [ + :difficulty, + :extra_data, + :gas_limit, + :gas_used, + :hash, + :logs_bloom, + :miner, + :nonce, + :number, + :parent_hash, + :receipt_root, + :sha3_uncles, + :size, + :state_root, + :timestamp, + :total_difficulty, + :transactions, + :transactions_root, + :uncles + ] + end + + test "get_block/1 by number works" do + Process.sleep(2) + + target_result = ETH.get_block(2) + target_block = target_result |> elem(1) + + assert target_result |> elem(0) == :ok + assert target_block.number == 2 + assert target_block.number != ETH.get_block!(1) |> Map.get(:number) + + assert Map.keys(target_block) == [ + :difficulty, + :extra_data, + :gas_limit, + :gas_used, + :hash, + :logs_bloom, + :miner, + :nonce, + :number, + :parent_hash, + :receipt_root, + :sha3_uncles, + :size, + :state_root, + :timestamp, + :total_difficulty, + :transactions, + :transactions_root, + :uncles + ] + end + + test "get_block/1 by hash works" do + Process.sleep(2) + {:ok, first_block} = ETH.get_block(1) + {:ok, second_block} = ETH.get_block(2) + + assert ETH.get_block(first_block.hash) |> elem(1) == first_block + assert ETH.get_block(second_block.hash) |> elem(1) == second_block + end + + test "get_block!/0 works" do + current_block = ETH.get_block!() + + assert current_block.number == ETH.block_number!() + + assert Map.keys(current_block) == [ + :difficulty, + :extra_data, + :gas_limit, + :gas_used, + :hash, + :logs_bloom, + :miner, + :nonce, + :number, + :parent_hash, + :receipt_root, + :sha3_uncles, + :size, + :state_root, + :timestamp, + :total_difficulty, + :transactions, + :transactions_root, + :uncles + ] + end + + test "get_block!/1 by number works" do + Process.sleep(2) + + target_block = ETH.get_block!(2) + + assert target_block.number == 2 + assert target_block.number != ETH.get_block!(1) |> Map.get(:number) + + assert Map.keys(target_block) == [ + :difficulty, + :extra_data, + :gas_limit, + :gas_used, + :hash, + :logs_bloom, + :miner, + :nonce, + :number, + :parent_hash, + :receipt_root, + :sha3_uncles, + :size, + :state_root, + :timestamp, + :total_difficulty, + :transactions, + :transactions_root, + :uncles + ] + end + + test "get_block!/1 by hash works" do + Process.sleep(2) + first_block = ETH.get_block!(1) + second_block = ETH.get_block!(2) + + assert ETH.get_block!(first_block.hash) == first_block + assert ETH.get_block!(second_block.hash) == second_block + end + test "get_balance/1 returns the balance of an ethereum address in ether by default" do - address_with_balance = ETH.Query.get_accounts |> List.last - address_with_no_balance = ETH.Wallet.create |> Map.get(:eth_address) + address_with_balance = ETH.get_accounts!() |> List.last() + address_with_no_balance = ETH.Wallet.create() |> Map.get(:eth_address) - assert ETH.Query.get_balance(address_with_balance) == 100.0 - assert ETH.Query.get_balance(address_with_no_balance) == 0.0 + assert ETH.get_balance(address_with_balance) == {:ok, 100.0} + assert ETH.get_balance(address_with_no_balance) == {:ok, 0.0} end test "balance/1 returns the balance of an ethereum address with specific denomination" do - address_with_balance = ETH.Query.get_accounts |> List.last - address_with_no_balance = ETH.Wallet.create |> Map.get(:eth_address) + address_with_balance = ETH.get_accounts!() |> List.last() + address_with_no_balance = ETH.Wallet.create() |> Map.get(:eth_address) + + assert ETH.get_balance(address_with_balance, :wei) == {:ok, 1.0e20} + assert ETH.get_balance(address_with_no_balance, :wei) == {:ok, 0.0} + end + + test "get_balance/1 returns the balance of an ethereum wallet in ether by default" do + wallet_with_balance = @first_client_account_private_key |> ETH.Wallet.create() + wallet_with_no_balance = ETH.Wallet.create() + + assert ETH.get_balance(wallet_with_balance) == {:ok, 100.0} + assert ETH.get_balance(wallet_with_no_balance) == {:ok, 0.0} + end + + test "get_balance/1 returns the balance of an ethereum wallet with specific denomination" do + wallet_with_balance = @first_client_account_private_key |> ETH.Wallet.create() + wallet_with_no_balance = ETH.Wallet.create() + + assert ETH.get_balance(wallet_with_balance, :wei) == {:ok, 1.0e20} + assert ETH.get_balance(wallet_with_no_balance, :wei) == {:ok, 0.0} + end + + test "get_balance!/1 returns the balance of an ethereum address in ether by default" do + address_with_balance = ETH.get_accounts!() |> List.last() + address_with_no_balance = ETH.Wallet.create() |> Map.get(:eth_address) - assert ETH.Query.get_balance(address_with_balance, :wei) == 1.0e20 - assert ETH.Query.get_balance(address_with_no_balance, :wei) == 0.0 + assert ETH.get_balance!(address_with_balance) == 100.0 + assert ETH.get_balance!(address_with_no_balance) == 0.0 end - test "transaction_count/1 works" do - address_with_balance = ETH.Query.get_accounts |> List.last + test "get_balance!/1 returns the balance of an ethereum address with specific denomination" do + address_with_balance = ETH.get_accounts!() |> List.last() + address_with_no_balance = ETH.Wallet.create() |> Map.get(:eth_address) - assert ETH.Query.get_transaction_count(address_with_balance) == 0 + assert ETH.get_balance!(address_with_balance, :wei) == 1.0e20 + assert ETH.get_balance!(address_with_no_balance, :wei) == 0.0 + end + + test "get_balance!/1 returns the balance of an ethereum wallet in ether by default" do + wallet_with_balance = @first_client_account_private_key |> ETH.Wallet.create() + wallet_with_no_balance = ETH.Wallet.create() + + assert ETH.get_balance!(wallet_with_balance) == 100.0 + assert ETH.get_balance!(wallet_with_no_balance) == 0.0 + end + + test "get_balance!/1 returns the balance of an ethereum wallet with specific denomination" do + wallet_with_balance = @first_client_account_private_key |> ETH.Wallet.create() + wallet_with_no_balance = ETH.Wallet.create() + + assert ETH.get_balance!(wallet_with_balance, :wei) == 1.0e20 + assert ETH.get_balance!(wallet_with_no_balance, :wei) == 0.0 end test "estimate_gas/2 works with default wei denomination" do - address_with_balance = ETH.Query.get_accounts |> List.last - - assert ETH.Query.estimate_gas(%{to: address_with_balance, data: ""}) == 2.1e4 - assert ETH.Query.estimate_gas(%{to: address_with_balance, data: "asd"}) == 21340 - end - - # test "estimate_gas/2 works with different denomination" do - # address = ETH.Query.get_accounts |> List.first - # first_gas_in_ether = ETH.Query.estimate_gas(%{to: address, data: ""}) - # second_gas_in_ether = ETH.Query.estimate_gas(%{to: address, data: "asd"}) - # - # first_gas_in_wei = ETH.Query.estimate_gas(%{to: address, data: ""}, :wei) - # second_gas_in_wei = ETH.Query.estimate_gas(%{to: address, data: "asd"}, :wei) - # - # assert first_gas_in_wei == 21000 - # assert second_gas_in_wei == 21340 - # - # first_difference = first_gas_in_ether / second_gas_in_ether - # second_difference = first_gas_in_wei / second_gas_in_wei - # assert Float.floor(first_difference, 15) == Float.floor(second_difference, 15) - # end + address_with_balance = ETH.get_accounts!() |> List.last() + + assert ETH.estimate_gas(%{to: address_with_balance, data: ""}) == {:ok, 2.1e4} + assert ETH.estimate_gas(%{to: address_with_balance, data: "asd"}) == {:ok, 21340} + end + + test "estimate_gas/2 works with different denomination" do + address = ETH.get_accounts!() |> List.first() + {:ok, first_gas_in_ether} = ETH.estimate_gas(%{to: address, data: ""}) + {:ok, second_gas_in_ether} = ETH.estimate_gas(%{to: address, data: "asd"}) + + {:ok, first_gas_in_wei} = ETH.estimate_gas(%{to: address, data: ""}, :wei) + {:ok, second_gas_in_wei} = ETH.estimate_gas(%{to: address, data: "asd"}, :wei) + + assert first_gas_in_wei == 21000 + assert second_gas_in_wei == 21340 + + first_difference = first_gas_in_ether / second_gas_in_ether + second_difference = first_gas_in_wei / second_gas_in_wei + assert Float.floor(first_difference, 15) == Float.floor(second_difference, 15) + end + + test "estimate_gas!/2 works with default wei denomination" do + address_with_balance = ETH.get_accounts!() |> List.last() + + assert ETH.estimate_gas!(%{to: address_with_balance, data: ""}) == 2.1e4 + assert ETH.estimate_gas!(%{to: address_with_balance, data: "asd"}) == 21340 + end + + test "estimate_gas!/2 works with different denomination" do + address = ETH.get_accounts!() |> List.first() + first_gas_in_ether = ETH.estimate_gas!(%{to: address, data: ""}) + second_gas_in_ether = ETH.estimate_gas!(%{to: address, data: "asd"}) + + first_gas_in_wei = ETH.estimate_gas!(%{to: address, data: ""}, :wei) + second_gas_in_wei = ETH.estimate_gas!(%{to: address, data: "asd"}, :wei) + + assert first_gas_in_wei == 21000 + assert second_gas_in_wei == 21340 + + first_difference = first_gas_in_ether / second_gas_in_ether + second_difference = first_gas_in_wei / second_gas_in_wei + assert Float.floor(first_difference, 15) == Float.floor(second_difference, 15) + end end diff --git a/test/eth/transaction_queries_test.exs b/test/eth/transaction_queries_test.exs new file mode 100644 index 0000000..0187e5b --- /dev/null +++ b/test/eth/transaction_queries_test.exs @@ -0,0 +1,37 @@ +defmodule ETH.TransactionQueries.Test do + use ExUnit.Case + + @first_client_account_private_key "a160512c1dc5c33eff6ef89aae083108dcdcabdbe463481949d327fc2ac6ac48" + + # TODO: ETH.get_block_transactions + + # TODO: ETH.get_block_transactions_count + + # TODO: get_transaction_from_block!(identifier, index), to: ETH.Query + + # TODO: get_transaction + + # TODO: get_transaction_receipt + + test "transaction_count/1 works" do + # TODO: have one with transactions + address_with_balance = ETH.get_accounts!() |> List.last() + + assert ETH.get_transaction_count(address_with_balance) == {:ok, 0} + + wallet_with_balance = @first_client_account_private_key |> ETH.Wallet.create() + + assert ETH.get_transaction_count(wallet_with_balance) == {:ok, 0} + end + + test "transaction_count!/1 works" do + # TODO: have one with transactions + address_with_balance = ETH.get_accounts!() |> List.last() + + assert ETH.get_transaction_count!(address_with_balance) == 0 + + wallet_with_balance = @first_client_account_private_key |> ETH.Wallet.create() + + assert ETH.get_transaction_count!(wallet_with_balance) == 0 + end +end diff --git a/test/eth/transaction_test.exs b/test/eth/transaction_test.exs index 3654e7a..d1bc915 100644 --- a/test/eth/transaction_test.exs +++ b/test/eth/transaction_test.exs @@ -1,4 +1,5 @@ require IEx + defmodule ETH.TransactionTest do use ExUnit.Case import ETH.Utils @@ -10,21 +11,22 @@ defmodule ETH.TransactionTest do to: "0x0000000000000000000000000000000000000000", value: "0x00", data: "0x7f7465737432000000000000000000000000000000000000000000000000000000600057", - chain_id: 3 # EIP 155 chainId - mainnet: 1, ropsten: 3 + # EIP 155 chainId - mainnet: 1, ropsten: 3 + chain_id: 3 } - @transactions File.read!("test/fixtures/transactions.json") |> Poison.decode! - @eip155_transactions File.read!("test/fixtures/eip155_vitalik_tests.json") |> Poison.decode! + @transactions File.read!("test/fixtures/transactions.json") |> Poison.decode!() + @eip155_transactions File.read!("test/fixtures/eip155_vitalik_tests.json") |> Poison.decode!() test "parse/1 and to_list/1 works for 0x hexed transactions" do @transactions |> Enum.slice(0..3) - |> Enum.map(fn(transaction) -> transaction["raw"] end) - |> Enum.each(fn(transaction) -> - transaction_list = transaction |> ETH.Transaction.parse |> ETH.Transaction.to_list + |> Enum.map(fn transaction -> transaction["raw"] end) + |> Enum.each(fn transaction -> + transaction_list = transaction |> ETH.Transaction.parse() |> ETH.Transaction.to_list() transaction_list - |> Stream.with_index - |> Enum.each(fn({_value, index}) -> + |> Stream.with_index() + |> Enum.each(fn {_value, index} -> encoded_buffer = Enum.at(transaction_list, index) |> Base.encode16(case: :lower) assert Enum.at(transaction, index) == "0x#{encoded_buffer}" end) @@ -33,47 +35,66 @@ defmodule ETH.TransactionTest do test "hash/1 works" do target_hash = "DF2A7CB6D05278504959987A144C116DBD11CBDC50D6482C5BAE84A7F41E2113" + assert @first_example_transaction - |> ETH.Transaction.to_list - |> List.insert_at(-1, @first_example_transaction.chain_id) - |> ETH.Transaction.hash(false) - |> Base.encode16 == target_hash + |> ETH.Transaction.to_list() + |> List.insert_at(-1, @first_example_transaction.chain_id) + |> ETH.Transaction.hash(false) + |> Base.encode16() == target_hash - first_transaction_list = @transactions + first_transaction_list = + @transactions |> Enum.at(2) |> Map.get("raw") - |> ETH.Transaction.parse - |> ETH.Transaction.to_list + |> ETH.Transaction.parse() + |> ETH.Transaction.to_list() - second_transaction_list = @transactions + second_transaction_list = + @transactions |> Enum.at(3) |> Map.get("raw") - |> ETH.Transaction.parse - |> ETH.Transaction.to_list + |> ETH.Transaction.parse() + |> ETH.Transaction.to_list() + + assert ETH.Transaction.hash(first_transaction_list) == + decode16("375a8983c9fc56d7cfd118254a80a8d7403d590a6c9e105532b67aca1efb97aa") + + assert ETH.Transaction.hash(first_transaction_list, false) == + decode16("61e1ec33764304dddb55348e7883d4437426f44ab3ef65e6da1e025734c03ff0") + + assert ETH.Transaction.hash(first_transaction_list, true) == + decode16("375a8983c9fc56d7cfd118254a80a8d7403d590a6c9e105532b67aca1efb97aa") + + assert ETH.Transaction.hash(second_transaction_list) == + decode16("0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4") - assert ETH.Transaction.hash(first_transaction_list) == decode16("375a8983c9fc56d7cfd118254a80a8d7403d590a6c9e105532b67aca1efb97aa") - assert ETH.Transaction.hash(first_transaction_list, false) == decode16("61e1ec33764304dddb55348e7883d4437426f44ab3ef65e6da1e025734c03ff0") - assert ETH.Transaction.hash(first_transaction_list, true) == decode16("375a8983c9fc56d7cfd118254a80a8d7403d590a6c9e105532b67aca1efb97aa") + assert ETH.Transaction.hash(second_transaction_list, true) == + decode16("0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4") - assert ETH.Transaction.hash(second_transaction_list) == decode16("0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4") - assert ETH.Transaction.hash(second_transaction_list, true) == decode16("0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4") - assert ETH.Transaction.hash(second_transaction_list, false) == decode16("f97c73fdca079da7652dbc61a46cd5aeef804008e057be3e712c43eac389aaf0") + assert ETH.Transaction.hash(second_transaction_list, false) == + decode16("f97c73fdca079da7652dbc61a46cd5aeef804008e057be3e712c43eac389aaf0") end test "secp256k1_signature/2 works" do - hash = "5c207a650b59a8c2d1271f5cbda78a658cb411a87271d68062e61ab1a3f85cf9" + hash = + "5c207a650b59a8c2d1271f5cbda78a658cb411a87271d68062e61ab1a3f85cf9" |> Base.decode16!(case: :mixed) - private_key = "e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109" + + private_key = + "e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109" |> Base.decode16!(case: :mixed) - target_signature = "c2a738b1eb84280399115f4bec9e52b8de494a3ea7d9f069277119a02de4a49876f3168913e968e9484e2e0e447cd7adc56505e25cbc372330793a31f0bf7195" + target_signature = + "c2a738b1eb84280399115f4bec9e52b8de494a3ea7d9f069277119a02de4a49876f3168913e968e9484e2e0e447cd7adc56505e25cbc372330793a31f0bf7195" + secp256k1_signature = ETH.Utils.secp256k1_signature(hash, private_key) assert secp256k1_signature[:signature] |> Base.encode16(case: :lower) == target_signature end test "hash_transaction/2 works" do - result = @first_example_transaction + result = + @first_example_transaction |> ETH.Transaction.hash_transaction(false) |> Base.encode16(case: :lower) @@ -83,8 +104,9 @@ defmodule ETH.TransactionTest do test "get_sender_address/1 works" do @transactions |> Enum.slice(0..2) - |> Enum.each(fn(transaction) -> - transaction_list = transaction |> Map.get("raw") |> ETH.Transaction.parse |> ETH.Transaction.to_list + |> Enum.each(fn transaction -> + transaction_list = + transaction |> Map.get("raw") |> ETH.Transaction.parse() |> ETH.Transaction.to_list() result = ETH.Transaction.get_sender_address(transaction_list) assert result == "0x" <> String.upcase(transaction["sendersAddress"]) @@ -94,11 +116,12 @@ defmodule ETH.TransactionTest do test "sign/2 works" do @transactions |> Enum.slice(0..2) - |> Enum.each(fn(transaction) -> - signed_transaction_list = transaction + |> Enum.each(fn transaction -> + signed_transaction_list = + transaction |> Map.get("raw") - |> ETH.Transaction.parse - |> ETH.Transaction.to_list + |> ETH.Transaction.parse() + |> ETH.Transaction.to_list() |> ETH.Transaction.sign_transaction_list(transaction["privateKey"]) result = ETH.Transaction.get_sender_address(signed_transaction_list) @@ -107,24 +130,34 @@ defmodule ETH.TransactionTest do end test "verify EIP155 Signature based on Vitalik\'s tests" do - @eip155_transactions |> Enum.each(fn(transaction) -> - transaction_list = transaction |> Map.get("rlp") |> ETH.Transaction.to_list + @eip155_transactions + |> Enum.each(fn transaction -> + transaction_list = transaction |> Map.get("rlp") |> ETH.Transaction.to_list() expected_hash = transaction["hash"] |> Base.decode16!(case: :lower) assert ETH.Transaction.hash(transaction_list, false) == expected_hash - sender_address = transaction["sender"] |> String.upcase + sender_address = transaction["sender"] |> String.upcase() assert ETH.Transaction.get_sender_address(transaction_list) == "0x#{sender_address}" end) end test "send_transaction_works" do - output = ETH.Transaction.set(%{ - nonce: 1, to: "0x0dcd857b3c5db88cb7c025f0ef229331cfadffe5", value: 22, gas_limit: 100000, - gas_price: 1000, from: "0x42c343d8b77a9106d7112b71ba6b3030a34ba560" - }) - |> ETH.Transaction.sign_transaction("75c3b11e480f8ba3db792424bebda1fc8dea2b254287e3a9af9ed50c7d255720") - |> Base.encode16(case: :lower) - - serialized_hash = "f862018203e8830186a0940dcd857b3c5db88cb7c025f0ef229331cfadffe516801ba09b35467cf48151683b41ed8425d59317716f4f639126d7eb69167ac95c8c3ba3a00d5d21f4c6fc400202dadc09a192b011cc16aefa6155d4e5df15d77d9f6c8f9f" + output = + ETH.Transaction.set(%{ + nonce: 1, + to: "0x0dcd857b3c5db88cb7c025f0ef229331cfadffe5", + value: 22, + gas_limit: 100_000, + gas_price: 1000, + from: "0x42c343d8b77a9106d7112b71ba6b3030a34ba560" + }) + |> ETH.Transaction.sign_transaction( + "75c3b11e480f8ba3db792424bebda1fc8dea2b254287e3a9af9ed50c7d255720" + ) + |> Base.encode16(case: :lower) + + serialized_hash = + "f862018203e8830186a0940dcd857b3c5db88cb7c025f0ef229331cfadffe516801ba09b35467cf48151683b41ed8425d59317716f4f639126d7eb69167ac95c8c3ba3a00d5d21f4c6fc400202dadc09a192b011cc16aefa6155d4e5df15d77d9f6c8f9f" + assert output == serialized_hash end diff --git a/test/eth/utils_test.exs b/test/eth/utils_test.exs index 7cb0d23..35ee6e3 100644 --- a/test/eth/utils_test.exs +++ b/test/eth/utils_test.exs @@ -2,8 +2,8 @@ defmodule ETH.UtilsTest do use ExUnit.Case test "get_private_key/0 works" do - assert ETH.Utils.get_private_key |> byte_size == 32 - assert ETH.Utils.get_private_key != ETH.Utils.get_private_key + assert ETH.Utils.get_private_key() |> byte_size == 32 + assert ETH.Utils.get_private_key() != ETH.Utils.get_private_key() end test "get_public_key/1 works" do @@ -15,15 +15,16 @@ defmodule ETH.UtilsTest do assert public_key == ETH.Utils.get_public_key(private_key) assert public_key != ETH.Utils.get_public_key(another_private_key) - 1..1000 |> Enum.each(fn(_) -> + 1..1000 + |> Enum.each(fn _ -> private_key = :crypto.strong_rand_bytes(32) ETH.Utils.get_public_key(private_key) end) end test "get_public_key/1 works for encoded private keys" do - private_key = :crypto.strong_rand_bytes(32) |> Base.encode16 - another_private_key = :crypto.strong_rand_bytes(32) |> Base.encode16 + private_key = :crypto.strong_rand_bytes(32) |> Base.encode16() + another_private_key = :crypto.strong_rand_bytes(32) |> Base.encode16() public_key = ETH.Utils.get_public_key(private_key) assert public_key |> byte_size == 65 @@ -45,21 +46,21 @@ defmodule ETH.UtilsTest do test "get_address/1 works for encoded private keys" do private_key = :crypto.strong_rand_bytes(32) another_private_key = :crypto.strong_rand_bytes(32) - eth_address = ETH.Utils.get_address(private_key |> Base.encode16) + eth_address = ETH.Utils.get_address(private_key |> Base.encode16()) assert eth_address |> byte_size == 42 - assert eth_address == ETH.Utils.get_address(private_key |> Base.encode16) + assert eth_address == ETH.Utils.get_address(private_key |> Base.encode16()) assert eth_address |> String.slice(0, 2) == "0x" assert eth_address != ETH.Utils.get_address(another_private_key) end test "get_address/1 works for public keys" do - private_key = :crypto.strong_rand_bytes(32) |> Base.encode16 + private_key = :crypto.strong_rand_bytes(32) |> Base.encode16() public_key = ETH.Utils.get_public_key(private_key) eth_address = ETH.Utils.get_address(public_key) - another_private_key = :crypto.strong_rand_bytes(32) |> Base.encode16 - another_public_key = ETH.Utils.get_public_key(another_private_key) |> Base.encode16 + another_private_key = :crypto.strong_rand_bytes(32) |> Base.encode16() + another_public_key = ETH.Utils.get_public_key(another_private_key) |> Base.encode16() another_eth_address = ETH.Utils.get_address(another_public_key) assert eth_address |> byte_size == 42 @@ -73,17 +74,17 @@ defmodule ETH.UtilsTest do test "get_address/1 works for encoded public keys" do private_key = :crypto.strong_rand_bytes(32) public_key = ETH.Utils.get_public_key(private_key) - eth_address = ETH.Utils.get_address(public_key |> Base.encode16) + eth_address = ETH.Utils.get_address(public_key |> Base.encode16()) another_private_key = :crypto.strong_rand_bytes(32) another_public_key = ETH.Utils.get_public_key(another_private_key) - another_eth_address = ETH.Utils.get_address(another_public_key |> Base.encode16) + another_eth_address = ETH.Utils.get_address(another_public_key |> Base.encode16()) assert eth_address |> byte_size == 42 - assert eth_address == ETH.Utils.get_address(public_key |> Base.encode16) - assert eth_address == ETH.Utils.get_address(private_key |> Base.encode16) + assert eth_address == ETH.Utils.get_address(public_key |> Base.encode16()) + assert eth_address == ETH.Utils.get_address(private_key |> Base.encode16()) assert eth_address |> String.slice(0, 2) == "0x" assert eth_address != another_eth_address - assert eth_address != ETH.Utils.get_address(another_private_key |> Base.encode16) + assert eth_address != ETH.Utils.get_address(another_private_key |> Base.encode16()) end end diff --git a/test/eth/wallet_test.exs b/test/eth/wallet_test.exs index b050d72..00edfeb 100644 --- a/test/eth/wallet_test.exs +++ b/test/eth/wallet_test.exs @@ -8,7 +8,10 @@ defmodule ETH.WalletTest do assert wallet[:private_key] assert wallet[:private_key] != second_wallet[:private_key] assert wallet[:public_key] - assert wallet[:public_key] == ETH.Utils.get_public_key(wallet[:private_key]) |> Base.encode16 + + assert wallet[:public_key] == + ETH.Utils.get_public_key(wallet[:private_key]) |> Base.encode16() + assert wallet[:public_key] != second_wallet[:public_key] assert wallet[:eth_address] assert wallet[:eth_address] == ETH.Utils.get_address(wallet[:public_key]) diff --git a/test/test_helper.exs b/test/test_helper.exs index c6da862..d6af375 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,3 +3,5 @@ Application.put_env(:ethereumex, :host, Application.get_env(:eth, :host, "localh Application.put_env(:ethereumex, :port, Application.get_env(:eth, :port, 8545)) ExUnit.start() + +# testrpc -b=1 -m=" parent leopard beauty edit tilt what blast next huge need print advice evolve move explain govern grab raccoon gown gravity gloom walnut silver reopen"