diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a54e83ba5..c1619da830 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -296,7 +296,7 @@ jobs: name: Scan explorer_web for vulnerabilities command: mix sobelow --config working_directory: "apps/explorer_web" - test: + test_geth: docker: # Ensure .tool-versions matches - image: circleci/elixir:1.6.5-node-browsers @@ -306,6 +306,7 @@ jobs: PGPASSWORD: postgres # match POSTGRES_USER for postgres image below PGUSER: postgres + ETHEREUM_JSONRPC_VARIANT: geth - image: circleci/postgres:10.3-alpine environment: # Match apps/explorer/config/test.exs config :explorerer, Explorer.Repo, database @@ -328,7 +329,44 @@ jobs: name: Wait for DB command: dockerize -wait tcp://localhost:5432 -timeout 1m - - run: mix coveralls.circle --umbrella + - run: mix coveralls.circle --exclude no_geth --parallel --umbrella + + - store_test_results: + path: _build/test/junit + test_parity: + docker: + # Ensure .tool-versions matches + - image: circleci/elixir:1.6.5-node-browsers + environment: + MIX_ENV: test + # match POSTGRES_PASSWORD for postgres image below + PGPASSWORD: postgres + # match POSTGRES_USER for postgres image below + PGUSER: postgres + ETHEREUM_JSONRPC_VARIANT: parity + - image: circleci/postgres:10.3-alpine + environment: + # Match apps/explorer/config/test.exs config :explorerer, Explorer.Repo, database + POSTGRES_DB: explorer_test + # match PGPASSWORD for elixir image above + POSTGRES_PASSWORD: postgres + # match PGUSER for elixir image above + POSTGRES_USER: postgres + + working_directory: ~/app + + steps: + - attach_workspace: + at: . + + - run: mix local.hex --force + - run: mix local.rebar --force + + - run: + name: Wait for DB + command: dockerize -wait tcp://localhost:5432 -timeout 1m + + - run: mix coveralls.circle --parallel --umbrella - store_test_results: path: _build/test/junit @@ -356,7 +394,8 @@ workflows: - eslint - jest - sobelow - - test + - test_parity + - test_geth - dialyzer: requires: - build @@ -372,6 +411,9 @@ workflows: - sobelow: requires: - build - - test: + - test_parity: + requires: + - build + - test_geth: requires: - build diff --git a/apps/ethereum_jsonrpc/.formatter.exs b/apps/ethereum_jsonrpc/.formatter.exs deleted file mode 100644 index 525446d406..0000000000 --- a/apps/ethereum_jsonrpc/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/ethereum_jsonrpc/config/config.exs b/apps/ethereum_jsonrpc/config/config.exs index a1c766314a..08a36ee4cd 100644 --- a/apps/ethereum_jsonrpc/config/config.exs +++ b/apps/ethereum_jsonrpc/config/config.exs @@ -3,6 +3,10 @@ use Mix.Config config :ethereum_jsonrpc, - http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]], - trace_url: "https://sokol-trace.poa.network", - url: "https://sokol.poa.network" + http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] + +variant = System.get_env("ETHEREUM_JSONRPC_VARIANT") || "parity" + +# Import variant specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{variant}.exs" diff --git a/apps/ethereum_jsonrpc/config/geth.exs b/apps/ethereum_jsonrpc/config/geth.exs new file mode 100644 index 0000000000..703b649cc5 --- /dev/null +++ b/apps/ethereum_jsonrpc/config/geth.exs @@ -0,0 +1,5 @@ +use Mix.Config + +config :ethereum_jsonrpc, + url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY", + variant: EthereumJSONRPC.Geth diff --git a/apps/ethereum_jsonrpc/config/parity.exs b/apps/ethereum_jsonrpc/config/parity.exs new file mode 100644 index 0000000000..5e1fb4ed89 --- /dev/null +++ b/apps/ethereum_jsonrpc/config/parity.exs @@ -0,0 +1,9 @@ +use Mix.Config + +config :ethereum_jsonrpc, + url: "https://sokol.poa.network", + method_to_url: [ + eth_getBalance: "https://sokol-trace.poa.network", + trace_replayTransaction: "https://sokol-trace.poa.network" + ], + variant: EthereumJSONRPC.Parity diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 41794ed328..dce2d8a241 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -19,7 +19,7 @@ defmodule EthereumJSONRPC do require Logger alias Explorer.Chain.Block - alias EthereumJSONRPC.{Blocks, Parity, Receipts, Transactions} + alias EthereumJSONRPC.{Blocks, Receipts, Transactions} @typedoc """ Truncated 20-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a @@ -89,6 +89,27 @@ defmodule EthereumJSONRPC do Application.fetch_env!(:ethereum_jsonrpc, key) end + @doc """ + Fetches the configured url for the specific `method` or the fallback `url` + + Configuration for a specific `method` can be set in `method_to_url` `config` + + config :ethereum_jsonrpc, + method_to_url: [ + eth_getBalance: "method_to_url" + ] + + The fallback 'url' MUST we set if not all methods have a url set. + + config :ethereum_jsonrpc, + url: + """ + def method_to_url(method) when is_atom(method) do + :ethereum_jsonrpc + |> Application.get_env(:method_to_url, []) + |> Keyword.get_lazy(method, fn -> config(:url) end) + end + @doc """ Fetches balance for each address `hash` at the `block_number` """ @@ -108,7 +129,7 @@ defmodule EthereumJSONRPC do with {:ok, responses} <- id_to_params |> get_balance_requests() - |> json_rpc(config(:trace_url)) do + |> json_rpc(method_to_url(:eth_getBalance)) do get_balance_responses_to_addresses_params(responses, id_to_params) end end @@ -121,7 +142,7 @@ defmodule EthereumJSONRPC do def fetch_blocks_by_hash(block_hashes) do block_hashes |> get_block_by_hash_requests() - |> json_rpc(config(:url)) + |> json_rpc(method_to_url(:eth_getBlockByHash)) |> handle_get_blocks() |> case do {:ok, _next, results} -> {:ok, results} @@ -135,18 +156,13 @@ defmodule EthereumJSONRPC do def fetch_blocks_by_range(_first.._last = range) do range |> get_block_by_number_requests() - |> json_rpc(config(:url)) + |> json_rpc(method_to_url(:eth_getBlockByNumber)) |> handle_get_blocks() end @doc """ Fetches block number by `t:tag/0`. - The `"earliest"` tag is the earlist block number, which is `0`. - - iex> EthereumJSONRPC.fetch_block_number_by_tag("earliest") - {:ok, 0} - ## Returns * `{:ok, number}` - the block number for the given `tag`. @@ -158,19 +174,29 @@ defmodule EthereumJSONRPC do def fetch_block_number_by_tag(tag) when tag in ~w(earliest latest pending) do tag |> get_block_by_tag_request() - |> json_rpc(config(:url)) + |> json_rpc(method_to_url(:eth_getBlockByNumber)) |> handle_get_block_by_tag() end @doc """ - Fetches internal transactions from client-specific API. + Fetches internal transactions from variant API. """ def fetch_internal_transactions(params_list) when is_list(params_list) do - Parity.fetch_internal_transactions(params_list) + config(:variant).fetch_internal_transactions(params_list) end - def fetch_transaction_receipts(hashes) when is_list(hashes) do - Receipts.fetch(hashes) + @doc """ + Fetches pending transactions from variant API. + """ + def fetch_pending_transactions do + config(:variant).fetch_pending_transactions() + end + + @spec fetch_transaction_receipts([ + %{required(:gas) => non_neg_integer(), required(:hash) => hash, optional(atom) => any} + ]) :: {:ok, %{logs: list(), receipts: list()}} | {:error, reason :: term} + def fetch_transaction_receipts(transactions_params) when is_list(transactions_params) do + Receipts.fetch(transactions_params) end @doc """ @@ -200,8 +226,14 @@ defmodule EthereumJSONRPC do json = encode_json(payload) case post(url, json, config(:http)) do - {:ok, %HTTPoison.Response{body: body, status_code: code}} -> - body |> decode_json(code, json, url) |> handle_response(code) + {:ok, %HTTPoison.Response{body: body, status_code: status_code}} -> + with {:ok, json} <- + decode_json( + request: [url: url, body: json], + response: [status_code: status_code, body: body] + ) do + handle_response(json, status_code) + end {:error, %HTTPoison.Error{reason: reason}} -> {:error, reason} @@ -272,8 +304,10 @@ defmodule EthereumJSONRPC do rechunk_json_rpc(url, chunks, options, response, decoded_response_bodies) {:ok, %HTTPoison.Response{body: body, status_code: status_code}} -> - decoded_body = decode_json(body, status_code, json, url) - chunked_json_rpc(url, tail, options, [decoded_body | decoded_response_bodies]) + with {:ok, decoded_body} <- + decode_json(request: [url: url, body: json], response: [status_code: status_code, body: body]) do + chunked_json_rpc(url, tail, options, [decoded_body | decoded_response_bodies]) + end {:error, %HTTPoison.Error{reason: reason}} -> {:error, reason} @@ -380,7 +414,7 @@ defmodule EthereumJSONRPC do defp get_block_by_tag_request(tag) do # eth_getBlockByNumber accepts either a number OR a tag - get_block_by_number_request(%{id: tag, tag: tag, transactions: :hashes}) + get_block_by_number_request(%{id: 1, tag: tag, transactions: :hashes}) end defp get_block_by_number_params(options) do @@ -412,25 +446,25 @@ defmodule EthereumJSONRPC do defp encode_json(data), do: Jason.encode_to_iodata!(data) - defp decode_json(response_body, response_status_code, request_body, request_url) do - Jason.decode!(response_body) - rescue - Jason.DecodeError -> - Logger.error(fn -> - """ - failed to decode json payload: - - request url: #{inspect(request_url)} + defp decode_json(named_arguments) when is_list(named_arguments) do + response = Keyword.fetch!(named_arguments, :response) + response_body = Keyword.fetch!(response, :body) - request body: #{inspect(request_body)} + with {:error, _} <- Jason.decode(response_body) do + case Keyword.fetch!(response, :status_code) do + # CloudFlare protected server return HTML errors for 502, so the JSON decode will fail + 502 -> + request_url = + named_arguments + |> Keyword.fetch!(:request) + |> Keyword.fetch!(:url) - response status code: #{inspect(response_status_code)} + {:error, {:bad_gateway, request_url}} - response body: #{inspect(response_body)} - """ - end) - - raise("bad jason") + _ -> + raise EthereumJSONRPC.DecodeError, named_arguments + end + end end defp handle_get_blocks({:ok, results}) do diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex index d7517279b0..9f90c66c66 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex @@ -4,7 +4,7 @@ defmodule EthereumJSONRPC.Block do and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber). """ - import EthereumJSONRPC, only: [nonce_to_integer: 1, quantity_to_integer: 1, timestamp_to_datetime: 1] + import EthereumJSONRPC, only: [quantity_to_integer: 1, timestamp_to_datetime: 1] alias EthereumJSONRPC alias EthereumJSONRPC.Transactions @@ -22,6 +22,8 @@ defmodule EthereumJSONRPC.Block do for the logs of the block. `nil` when block is pending. * `"miner"` - `t:EthereumJSONRPC.address/0` of the beneficiary to whom the mining rewards were given. Aliased by `"author"`. + * `"mixHash"` - Generated from [DAG](https://ethereum.stackexchange.com/a/10353) as part of Proof-of-Work for EthHash + algorithm. **[Geth](https://github.com/ethereum/go-ethereum/wiki/geth) + Proof-of-Work-only** * `"nonce"` - `t:EthereumJSONRPC.nonce/0`. `nil` when its pending block. * `"number"` - the block number `t:EthereumJSONRPC.quantity/0`. `nil` when block is pending. * `"parentHash" - the `t:EthereumJSONRPC.hash/0` of the parent block. @@ -92,11 +94,50 @@ defmodule EthereumJSONRPC.Block do total_difficulty: 340282366920938463463374607431465668165 } + [Geth] `elixir` can be converted to params + + iex> EthereumJSONRPC.Block.elixir_to_params( + ...> %{ + ...> "difficulty" => 17561410778, + ...> "extraData" => "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32", + ...> "gasLimit" => 5000, + ...> "gasUsed" => 0, + ...> "hash" => "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623", + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "miner" => "0xbb7b8287f3f0a933474a79eae42cbca977791171", + ...> "mixHash" => "0xbbb93d610b2b0296a59f18474ac3d6086a9902aa7ca4b9a306692f7c3d496fdf", + ...> "nonce" => 5539500215739777653, + ...> "number" => 59, + ...> "parentHash" => "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e", + ...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + ...> "size" => 542, + ...> "stateRoot" => "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba", + ...> "timestamp" => Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), + ...> "totalDifficulty" => 1039309006117, + ...> "transactions" => [], + ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "uncles" => [] + ...> } + ...> ) + %{ + difficulty: 17561410778, + gas_limit: 5000, + gas_used: 0, + hash: "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623", + miner_hash: "0xbb7b8287f3f0a933474a79eae42cbca977791171", + nonce: 5539500215739777653, + number: 59, + parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e", + size: 542, + timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), + total_difficulty: 1039309006117 + } + """ @spec elixir_to_params(elixir) :: map def elixir_to_params( %{ - "author" => miner_hash, "difficulty" => difficulty, "gasLimit" => gas_limit, "gasUsed" => gas_used, @@ -281,14 +322,10 @@ defmodule EthereumJSONRPC.Block do # `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct # hash format defp entry_to_elixir({key, _} = entry) - when key in ~w(author extraData hash logsBloom miner parentHash receiptsRoot sealFields sha3Uncles signature - stateRoot step transactionsRoot uncles), + when key in ~w(author extraData hash logsBloom miner mixHash nonce parentHash receiptsRoot sealFields sha3Uncles + signature stateRoot step transactionsRoot uncles), do: entry - defp entry_to_elixir({"nonce" = key, nonce}) do - {key, nonce_to_integer(nonce)} - end - defp entry_to_elixir({"timestamp" = key, timestamp}) do {key, timestamp_to_datetime(timestamp)} end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/decode_error.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/decode_error.ex new file mode 100644 index 0000000000..a3943ef50e --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/decode_error.ex @@ -0,0 +1,59 @@ +defmodule EthereumJSONRPC.DecodeError do + @moduledoc """ + An error has occurred decoding the response to an `EthereumJSONRPC.json_rpc` request. + """ + + @enforce_keys [:request, :response] + defexception [:request, :response] + + defmodule Request do + @moduledoc """ + Ethereum JSONRPC request whose `EthererumJSONRPC.DecodeError.Response` had a decode error. + """ + + @enforce_keys [:url, :body] + defstruct [:url, :body] + end + + defmodule Response do + @moduledoc """ + Ethereum JSONRPC response that had a decode error. + """ + + @enforce_keys [:status_code, :body] + defstruct [:status_code, :body] + end + + @impl Exception + def exception(named_arguments) do + request_fields = Keyword.fetch!(named_arguments, :request) + request = struct!(EthereumJSONRPC.DecodeError.Request, request_fields) + + response_fields = Keyword.fetch!(named_arguments, :response) + response = struct!(EthereumJSONRPC.DecodeError.Response, response_fields) + + %EthereumJSONRPC.DecodeError{request: request, response: response} + end + + @impl Exception + def message(%EthereumJSONRPC.DecodeError{ + request: %EthereumJSONRPC.DecodeError.Request{url: request_url, body: request_body}, + response: %EthereumJSONRPC.DecodeError.Response{status_code: response_status_code, body: response_body} + }) do + """ + Failed to decode Ethereum JSONRPC response: + + request: + + url: #{request_url} + + body: #{IO.iodata_to_binary(request_body)} + + response: + + status code: #{response_status_code} + + body: #{response_body} + """ + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex new file mode 100644 index 0000000000..4e8ee5bc04 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -0,0 +1,34 @@ +defmodule EthereumJSONRPC.Geth do + @moduledoc """ + Ethereum JSONRPC methods that are only supported by [Geth](https://github.com/ethereum/go-ethereum/wiki/geth). + """ + + @behaviour EthereumJSONRPC.Variant + + @doc """ + Internal transaction fetching is not supported currently for Geth. + + To signal to the caller that fetching is not supported, `:ignore` is returned + + iex> EthereumJSONRPC.Geth.fetch_internal_transactions([ + ...> "0x2ec382949ba0b22443aa4cb38267b1fb5e68e188109ac11f7a82f67571a0adf3" + ...> ]) + :ignore + + """ + @impl EthereumJSONRPC.Variant + def fetch_internal_transactions(transaction_params) when is_list(transaction_params), + do: :ignore + + @doc """ + Pending transaction fetching is not supported currently for Geth. + + To signal to the caller that fetching is not supported, `:ignore` is returned + + iex> EthereumJSONRPC.Geth.fetch_pending_transactions() + :ignore + + """ + @impl EthereumJSONRPC.Variant + def fetch_pending_transactions, do: :ignore +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex index 4876c749f3..55b327ee0e 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex @@ -50,25 +50,55 @@ defmodule EthereumJSONRPC.Log do type: "mined" } + Geth does not supply a `"type"` + + iex> EthereumJSONRPC.Log.elixir_to_params( + ...> %{ + ...> "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55", + ...> "blockHash" => "0x0b89f7f894f5d8ba941e16b61490e999a0fcaaf92dfcc70aee2ac5ddb5f243e1", + ...> "blockNumber" => 4448, + ...> "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + ...> "logIndex" => 0, + ...> "removed" => false, + ...> "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", + ...> "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", + ...> "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"], + ...> "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e", + ...> "transactionIndex" => 0 + ...> } + ...> ) + %{ + address_hash: "0xda8b3276cde6d768a44b9dac659faa339a41ac55", + block_number: 4448, + data: "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + first_topic: "0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", + fourth_topic: nil, + index: 0, + second_topic: "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", + third_topic: "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed", + transaction_hash: "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e" + } + """ - def elixir_to_params(%{ - "address" => address_hash, - "blockNumber" => block_number, - "data" => data, - "logIndex" => index, - "topics" => topics, - "transactionHash" => transaction_hash, - "type" => type - }) do + def elixir_to_params( + %{ + "address" => address_hash, + "blockNumber" => block_number, + "data" => data, + "logIndex" => index, + "topics" => topics, + "transactionHash" => transaction_hash + } = elixir + ) do %{ address_hash: address_hash, block_number: block_number, data: data, index: index, - transaction_hash: transaction_hash, - type: type + transaction_hash: transaction_hash } |> put_topics(topics) + |> put_type(elixir) end @doc """ @@ -101,6 +131,37 @@ defmodule EthereumJSONRPC.Log do "type" => "mined" } + Geth and Parity >= 1.11.4 includes a `"removed"` key + + iex> EthereumJSONRPC.Log.to_elixir( + ...> %{ + ...> "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55", + ...> "blockHash" => "0x0b89f7f894f5d8ba941e16b61490e999a0fcaaf92dfcc70aee2ac5ddb5f243e1", + ...> "blockNumber" => "0x1160", + ...> "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + ...> "logIndex" => "0x0", + ...> "removed" => false, + ...> "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", + ...> "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", + ...> "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"], + ...> "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e", + ...> "transactionIndex" => "0x0" + ...> } + ...> ) + %{ + "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55", + "blockHash" => "0x0b89f7f894f5d8ba941e16b61490e999a0fcaaf92dfcc70aee2ac5ddb5f243e1", + "blockNumber" => 4448, + "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + "logIndex" => 0, + "removed" => false, + "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130", + "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a", + "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"], + "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e", + "transactionIndex" => 0 + } + """ def to_elixir(log) when is_map(log) do Enum.into(log, %{}, &entry_to_elixir/1) @@ -120,4 +181,10 @@ defmodule EthereumJSONRPC.Log do |> Map.put(:third_topic, Enum.at(topics, 2)) |> Map.put(:fourth_topic, Enum.at(topics, 3)) end + + defp put_type(params, %{"type" => type}) do + Map.put(params, :type, type) + end + + defp put_type(params, _), do: params end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex index 7cdc7297c6..5ce44e10bf 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex @@ -3,21 +3,24 @@ defmodule EthereumJSONRPC.Parity do Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/). """ - import EthereumJSONRPC, only: [config: 1, id_to_params: 1, json_rpc: 2, request: 1] + import EthereumJSONRPC, only: [id_to_params: 1, method_to_url: 1, json_rpc: 2, request: 1] alias EthereumJSONRPC.Parity.Traces alias EthereumJSONRPC.{Transaction, Transactions} + @behaviour EthereumJSONRPC.Variant + @doc """ Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Parity trace URL. """ + @impl EthereumJSONRPC.Variant def fetch_internal_transactions(transactions_params) when is_list(transactions_params) do id_to_params = id_to_params(transactions_params) with {:ok, responses} <- id_to_params |> trace_replay_transaction_requests() - |> json_rpc(config(:trace_url)) do + |> json_rpc(method_to_url(:trace_replayTransaction)) do trace_replay_transaction_responses_to_internal_transactions_params(responses, id_to_params) end end @@ -28,12 +31,13 @@ defmodule EthereumJSONRPC.Parity do *NOTE*: The pending transactions are local to the node that is contacted and may not be consistent across nodes based on the transactions that each node has seen and how each node prioritizes collating transactions into the next block. """ + @impl EthereumJSONRPC.Variant @spec fetch_pending_transactions() :: {:ok, [Transaction.params()]} | {:error, reason :: term} def fetch_pending_transactions do with {:ok, transactions} <- %{id: 1, method: "parity_pendingTransactions", params: []} |> request() - |> json_rpc(config(:url)) do + |> json_rpc(method_to_url(:parity_pendingTransactions)) do transactions_params = transactions |> Transactions.to_elixir() diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex index 54d3278e25..f5ba7d2e21 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex @@ -19,12 +19,14 @@ defmodule EthereumJSONRPC.Receipt do * `"blockNumber"` - The block number `t:EthereumJSONRPC.quanity/0`. * `"cumulativeGasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used when this transaction was executed in the block. + * `"from"` - The `EthereumJSONRPC.Transaction.t/0` `"from"` address hash. **Geth-only.** * `"gasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used by this specific transaction alone. * `"logs"` - `t:list/0` of log objects, which this transaction generated. * `"logsBloom"` - `t:EthereumJSONRPC.data/0` of 256 Bytes for [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) for light clients to quickly retrieve related logs. * `"root"` - `t:EthereumJSONRPC.hash/0` of post-transaction stateroot (pre-Byzantium) * `"status"` - `t:EthereumJSONRPC.quantity/0` of either 1 (success) or 0 (failure) (post-Byzantium) + * `"to"` - The `EthereumJSONRPC.Transaction.t/0` `"to"` address hash. **Geth-only.** * `"transactionHash"` - `t:EthereumJSONRPC.hash/0` the transaction. * `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the transaction index in the block. """ @@ -70,6 +72,87 @@ defmodule EthereumJSONRPC.Receipt do transaction_index: 0 } + Geth, when showing pre-[Byzantium](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-609.md) does not include + the [status](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-658.md) as that was a post-Byzantium + [EIP](https://github.com/ethereum/EIPs/tree/master/EIPS). + + Pre-Byzantium receipts are given a derived `:status`: + + * If `"gas"` (supplied by caller from `EthereumJSONRPC.Transaction.elixir`) `==` `"gasUsed"`, then `:status` is + `:error` + + iex> EthereumJSONRPC.Receipt.elixir_to_params( + ...> %{ + ...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + ...> "blockNumber" => 46147, + ...> "contractAddress" => nil, + ...> "cumulativeGasUsed" => 21000, + ...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + ...> "gas" => 21000, + ...> "gasUsed" => 21000, + ...> "logs" => [], + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957", + ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + ...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + ...> "transactionIndex" => 0 + ...> } + ...> ) + %{ + cumulative_gas_used: 21000, + gas_used: 21000, + status: :error, + transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + transaction_index: 0 + } + + * Otherwise, `:status` is `:ok` + + iex> EthereumJSONRPC.Receipt.elixir_to_params( + ...> %{ + ...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + ...> "blockNumber" => 46147, + ...> "contractAddress" => nil, + ...> "cumulativeGasUsed" => 21000, + ...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + ...> "gas" => 40000, + ...> "gasUsed" => 21000, + ...> "logs" => [], + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957", + ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + ...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + ...> "transactionIndex" => 0 + ...> } + ...> ) + %{ + cumulative_gas_used: 21000, + gas_used: 21000, + status: :ok, + transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + transaction_index: 0 + } + + It is a developer error if the budgeted `"gas"` is not supplied for deriving the pre-Byzantium `:status`. + + iex> EthereumJSONRPC.Receipt.elixir_to_params( + ...> %{ + ...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + ...> "blockNumber" => 46147, + ...> "contractAddress" => nil, + ...> "cumulativeGasUsed" => 21000, + ...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + ...> "gasUsed" => 21000, + ...> "logs" => [], + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957", + ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + ...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + ...> "transactionIndex" => 0 + ...> } + ...> ) + ** (ArgumentError) Pre-Byzantium transaction receipts require the transaction gas to be given to derive their status + """ @spec elixir_to_params(elixir) :: %{ cumulative_gas_used: non_neg_integer, @@ -78,13 +161,16 @@ defmodule EthereumJSONRPC.Receipt do transaction_hash: String.t(), transaction_index: non_neg_integer() } - def elixir_to_params(%{ - "cumulativeGasUsed" => cumulative_gas_used, - "gasUsed" => gas_used, - "status" => status, - "transactionHash" => transaction_hash, - "transactionIndex" => transaction_index - }) do + def elixir_to_params( + %{ + "cumulativeGasUsed" => cumulative_gas_used, + "gasUsed" => gas_used, + "transactionHash" => transaction_hash, + "transactionIndex" => transaction_index + } = elixir + ) do + status = elixir_to_status(elixir) + %{ cumulative_gas_used: cumulative_gas_used, gas_used: gas_used, @@ -126,19 +212,77 @@ defmodule EthereumJSONRPC.Receipt do "transactionIndex" => 0 } + Receipts from Geth also supply the `EthereumJSONRPC.Transaction.t/0` `"from"` and `"to"` address hashes. + + iex> EthereumJSONRPC.Receipt.to_elixir( + ...> %{ + ...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + ...> "blockNumber" => "0xb443", + ...> "contractAddress" => nil, + ...> "cumulativeGasUsed" => "0x5208", + ...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + ...> "gasUsed" => "0x5208", + ...> "logs" => [], + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957", + ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + ...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + ...> "transactionIndex" => "0x0" + ...> } + ...> ) + %{ + "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + "blockNumber" => 46147, + "contractAddress" => nil, + "cumulativeGasUsed" => 21000, + "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + "gasUsed" => 21000, + "logs" => [], + "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957", + "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + "transactionIndex" => 0 + } + """ @spec to_elixir(t) :: elixir def to_elixir(receipt) when is_map(receipt) do Enum.into(receipt, %{}, &entry_to_elixir/1) end + defp elixir_to_status(elixir) do + case elixir do + %{"status" => status} -> + status + + %{"gas" => gas, "gasUsed" => gas_used} -> + pre_byzantium_status(gas, gas_used) + + _ -> + raise ArgumentError, + "Pre-Byzantium transaction receipts require the transaction gas to be given to derive their status" + end + end + + defp pre_byzantium_status(gas, gas_used) when is_integer(gas) and is_integer(gas_used) do + if gas_used < gas do + :ok + else + :error + end + end + # double check that no new keys are being missed by requiring explicit match for passthrough # `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct # hash format - defp entry_to_elixir({key, _} = entry) when key in ~w(blockHash contractAddress logsBloom root transactionHash), - do: entry + # gas is passsed in from the `t:EthereumJSONRPC.Transaction.params/0` to allow pre-Byzantium status to be derived + defp entry_to_elixir({key, _} = entry) + when key in ~w(blockHash contractAddress from gas logsBloom root to transactionHash), + do: entry - defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex) do + defp entry_to_elixir({key, quantity}) + when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex) do {key, quantity_to_integer(quantity)} end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex index 83a704c36f..4db6f7318a 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex @@ -111,15 +111,31 @@ defmodule EthereumJSONRPC.Receipts do Enum.map(elixir, &Receipt.elixir_to_params/1) end - def fetch(hashes) when is_list(hashes) do - hashes - |> Enum.map(&hash_to_json/1) + @spec fetch([ + %{ + required(:gas) => non_neg_integer(), + required(:hash) => EthereumJSONRPC.hash(), + optional(atom) => any + } + ]) :: {:ok, %{logs: list(), receipts: list()}} | {:error, reason :: term} + def fetch(transactions_params) when is_list(transactions_params) do + {requests, id_to_transaction_params} = + transactions_params + |> Stream.with_index() + |> Enum.reduce({[], %{}}, fn {%{hash: transaction_hash} = transaction_params, id}, + {acc_requests, acc_id_to_transaction_params} -> + requests = [request(id, transaction_hash) | acc_requests] + id_to_transaction_params = Map.put(acc_id_to_transaction_params, id, transaction_params) + {requests, id_to_transaction_params} + end) + + requests |> json_rpc(config(:url)) |> case do {:ok, responses} -> elixir_receipts = responses - |> responses_to_receipts() + |> responses_to_receipts(id_to_transaction_params) |> to_elixir() elixir_logs = elixir_to_logs(elixir_receipts) @@ -199,18 +215,29 @@ defmodule EthereumJSONRPC.Receipts do Enum.map(receipts, &Receipt.to_elixir/1) end - defp hash_to_json(hash) do + defp request(id, transaction_hash) when is_integer(id) and is_binary(transaction_hash) do %{ - "id" => hash, + "id" => id, "jsonrpc" => "2.0", "method" => "eth_getTransactionReceipt", - "params" => [hash] + "params" => [transaction_hash] } end - defp response_to_receipt(%{"result" => receipt}), do: receipt + defp response_to_receipt(%{"result" => nil}, _), do: %{} - defp responses_to_receipts(responses) when is_list(responses) do - Enum.map(responses, &response_to_receipt/1) + defp response_to_receipt(%{"id" => id, "result" => receipt}, id_to_transaction_params) do + gas = + id_to_transaction_params + |> Map.fetch!(id) + |> Map.fetch!(:gas) + + # gas from the transaction is needed for pre-Byzantium derived status + Map.put(receipt, "gas", gas) + end + + defp responses_to_receipts(responses, id_to_transaction_params) + when is_list(responses) and is_map(id_to_transaction_params) do + Enum.map(responses, &response_to_receipt(&1, id_to_transaction_params)) end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index 6e67db3dda..3cd8a67b87 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -56,15 +56,52 @@ defmodule EthereumJSONRPC.Transaction do index: non_neg_integer(), input: String.t(), nonce: non_neg_integer(), - public_key: String.t(), r: non_neg_integer(), s: non_neg_integer(), - standard_v: 0 | 1, to_address_hash: EthereumJSONRPC.address(), v: non_neg_integer(), value: non_neg_integer() } + @doc """ + Geth `elixir` can be converted to `params`. Geth does not supply `"publicKey"` or `"standardV"`, unlike Parity. + + iex> EthereumJSONRPC.Transaction.elixir_to_params( + ...> %{ + ...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + ...> "blockNumber" => 46147, + ...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + ...> "gas" => 21000, + ...> "gasPrice" => 50000000000000, + ...> "hash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + ...> "input" => "0x", + ...> "nonce" => 0, + ...> "r" => 61965845294689009770156372156374760022787886965323743865986648153755601564112, + ...> "s" => 31606574786494953692291101914709926755545765281581808821704454381804773090106, + ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + ...> "transactionIndex" => 0, + ...> "v" => 28, + ...> "value" => 31337 + ...> } + ...> ) + %{ + block_hash: "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + block_number: 46147, + from_address_hash: "0xa1e4380a3b1f749673e270229993ee55f35663b4", + gas: 21000, + gas_price: 50000000000000, + hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + index: 0, + input: "0x", + nonce: 0, + r: 61965845294689009770156372156374760022787886965323743865986648153755601564112, + s: 31606574786494953692291101914709926755545765281581808821704454381804773090106, + to_address_hash: "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + v: 28, + value: 31337 + } + + """ @spec elixir_to_params(elixir) :: params def elixir_to_params(%{ "blockHash" => block_hash, @@ -75,10 +112,8 @@ defmodule EthereumJSONRPC.Transaction do "hash" => hash, "input" => input, "nonce" => nonce, - "publicKey" => public_key, "r" => r, "s" => s, - "standardV" => standard_v, "to" => to_address_hash, "transactionIndex" => index, "v" => v, @@ -94,10 +129,8 @@ defmodule EthereumJSONRPC.Transaction do index: index, input: input, nonce: nonce, - public_key: public_key, r: r, s: s, - standard_v: standard_v, to_address_hash: to_address_hash, v: v, value: value @@ -117,10 +150,8 @@ defmodule EthereumJSONRPC.Transaction do ...> index: 0, ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> nonce: 0, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75", ...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3", - ...> standard_v: 0, ...> v: "0x8d", ...> value: 0 ...> } diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex index bedd4215ef..d3cae4a83b 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex @@ -50,10 +50,8 @@ defmodule EthereumJSONRPC.Transactions do index: 0, input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", nonce: 0, - public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", r: "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75", s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3", - standard_v: "0x0", to_address_hash: nil, v: "0xbd", value: 0 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex new file mode 100644 index 0000000000..2008c270d2 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex @@ -0,0 +1,35 @@ +defmodule EthereumJSONRPC.Variant do + @moduledoc """ + A variant of the Ethereum JSONRPC API. Each Ethereum client supports slightly different versions of the non-standard + Ethereum JSONRPC API. The variant callbacks abstract over this difference. + """ + + alias EthereumJSONRPC.Transaction + + @type internal_transaction_params :: map() + + @doc """ + Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the variant of the Ethereum JSONRPC API. + + ## Returns + + * `{:ok, [internal_transaction_params]}` - internal transactions were successfully fetched for all transactions + * `{:error, reason}` - there was one or more errors with `reason` in fetching at least one of the transaction's + internal transactions + * `:ignore` - the variant does not support fetching internal transactions. + """ + @callback fetch_internal_transactions([Transaction.params()]) :: + {:ok, [internal_transaction_params]} | {:error, reason :: term} | :ignore + + @doc """ + Fetch the `t:Explorer.Chain.Transaction.changeset/2` params for pending transactions from the variant of the Ethereum + JSONRPC API. + + ## Returns + + * `{:ok, [transaction_params]}` - pending transactions were succucessfully fetched + * `{:error, reason}` - there was one or more errors with `reason` in fetching the pending transactions + * `:ignore` - the variant does not support fetching pending transactions. + """ + @callback fetch_pending_transactions() :: {:ok, [Transaction.params()]} | {:error, reason :: term} | :ignore +end diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index cdd6428a9d..2429e31518 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -15,6 +15,7 @@ defmodule EthereumJsonrpc.MixProject do ignore_warnings: "../../.dialyzer-ignore" ], elixir: "~> 1.6", + elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", preferred_cli_env: [ coveralls: :test, @@ -45,6 +46,10 @@ defmodule EthereumJsonrpc.MixProject do ] ++ env_aliases(env) end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["test/support" | elixirc_paths(:dev)] + defp elixirc_paths(_), do: ["lib"] + defp env_aliases(:dev), do: [] defp env_aliases(_env), do: [compile: "compile --warnings-as-errors"] diff --git a/apps/ethereum_jsonrpc/test/etheream_jsonrpc_test.exs b/apps/ethereum_jsonrpc/test/etheream_jsonrpc_test.exs index bfacbf41d5..25c7333dba 100644 --- a/apps/ethereum_jsonrpc/test/etheream_jsonrpc_test.exs +++ b/apps/ethereum_jsonrpc/test/etheream_jsonrpc_test.exs @@ -1,130 +1,134 @@ defmodule EthereumJSONRPCTest do use ExUnit.Case, async: true - doctest EthereumJSONRPC + import EthereumJSONRPC.Case - describe "fetch_balances/1" do - test "with all valid hash_data returns {:ok, addresses_params}" do - assert EthereumJSONRPC.fetch_balances([ - %{block_quantity: "0x1", hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"} - ]) == - {:ok, - [ - %{ - fetched_balance: 1, - fetched_balance_block_number: 1, - hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" - } - ]} - end - - test "with all invalid hash_data returns {:error, reasons}" do - assert EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}]) == - {:error, - [ - %{ - "blockNumber" => "0x1", - "code" => -32602, - "hash" => "0x0", - "message" => - "Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." - } - ]} - end + @moduletag :capture_log - test "with a mix of valid and invalid hash_data returns {:error, reasons}" do - assert EthereumJSONRPC.fetch_balances([ - # start with :ok - %{ - block_quantity: "0x1", - hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" - }, - # :ok, :ok clause - %{ - block_quantity: "0x34", - hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" - }, - # :ok, :error clause - %{ - block_quantity: "0x2", - hash_data: "0x3" - }, - # :error, :ok clause - %{ - block_quantity: "0x35", - hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" - }, - # :error, :error clause - %{ - block_quantity: "0x4", - hash_data: "0x5" - } - ]) == - {:error, - [ - %{ - "blockNumber" => "0x2", - "code" => -32602, - "hash" => "0x3", - "message" => - "Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." - }, - %{ - "blockNumber" => "0x4", - "code" => -32602, - "hash" => "0x5", - "message" => - "Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." - } - ]} - end + setup do + %{variant: EthereumJSONRPC.config(:variant)} end - describe "json_rpc/2" do - # regression test for https://github.com/poanetwork/poa-explorer/issues/254 - test "transparently splits batch payloads that would trigger a 413 Request Entity Too Large" do - block_numbers = 0..13000 + describe "fetch_balances/1" do + test "with all valid hash_data returns {:ok, addresses_params}", %{variant: variant} do + assert {:ok, + [ + %{ + fetched_balance: fetched_balance, + fetched_balance_block_number: 1, + hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + ]} = + EthereumJSONRPC.fetch_balances([ + %{block_quantity: "0x1", hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"} + ]) + + case variant do + EthereumJSONRPC.Geth -> + assert fetched_balance == 0 + + EthereumJSONRPC.Parity -> + assert fetched_balance == 1 + + _ -> + raise ArgumentError, "Unsupported variant (#{variant}})" + end + end - payload = - block_numbers - |> Stream.with_index() - |> Enum.map(&get_block_by_number_request/1) + test "with all invalid hash_data returns {:error, reasons}", %{variant: variant} do + assert {:error, reasons} = EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}]) + assert is_list(reasons) + assert length(reasons) == 1 - assert_payload_too_large(payload) + [reason] = reasons - url = EthereumJSONRPC.config(:url) + assert %{ + "blockNumber" => "0x1", + "code" => -32602, + "hash" => "0x0", + "message" => message + } = reason - assert {:ok, responses} = EthereumJSONRPC.json_rpc(payload, url) - assert Enum.count(responses) == Enum.count(block_numbers) + case variant do + EthereumJSONRPC.Geth -> + assert message == + "invalid argument 0: json: cannot unmarshal hex string of odd length into Go value of type common.Address" - block_number_set = MapSet.new(block_numbers) + EthereumJSONRPC.Parity -> + assert message == + "Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." - response_block_number_set = - Enum.into(responses, MapSet.new(), fn %{"result" => %{"number" => quantity}} -> - EthereumJSONRPC.quantity_to_integer(quantity) - end) + _ -> + raise ArgumentError, "Unsupported variant (#{variant}})" + end + end - assert MapSet.equal?(response_block_number_set, block_number_set) + test "with a mix of valid and invalid hash_data returns {:error, reasons}" do + assert {:error, reasons} = + EthereumJSONRPC.fetch_balances([ + # start with :ok + %{ + block_quantity: "0x1", + hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + }, + # :ok, :ok clause + %{ + block_quantity: "0x34", + hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + }, + # :ok, :error clause + %{ + block_quantity: "0x2", + hash_data: "0x3" + }, + # :error, :ok clause + %{ + block_quantity: "0x35", + hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + }, + # :error, :error clause + %{ + block_quantity: "0x4", + hash_data: "0x5" + } + ]) + + assert is_list(reasons) + assert length(reasons) > 1 end end - defp assert_payload_too_large(payload) do - json = Jason.encode_to_iodata!(payload) - headers = [{"Content-Type", "application/json"}] - url = EthereumJSONRPC.config(:url) - - assert {:ok, %HTTPoison.Response{body: body, status_code: 413}} = - HTTPoison.post(url, json, headers, EthereumJSONRPC.config(:http)) + describe "fetch_block_number_by_tag/1" do + @tag capture_log: false + test "with earliest" do + log_bad_gateway( + fn -> EthereumJSONRPC.fetch_block_number_by_tag("earliest") end, + fn result -> + assert {:ok, 0} = result + end + ) + end - assert body =~ "413 Request Entity Too Large" - end + @tag capture_log: false + test "with latest" do + log_bad_gateway( + fn -> EthereumJSONRPC.fetch_block_number_by_tag("latest") end, + fn result -> + assert {:ok, number} = result + assert number > 0 + end + ) + end - defp get_block_by_number_request({block_number, id}) do - %{ - "id" => id, - "jsonrpc" => "2.0", - "method" => "eth_getBlockByNumber", - "params" => [EthereumJSONRPC.integer_to_quantity(block_number), true] - } + @tag capture_log: false + test "with pending" do + log_bad_gateway( + fn -> EthereumJSONRPC.fetch_block_number_by_tag("pending") end, + fn result -> + assert {:ok, number} = result + assert number > 0 + end + ) + end end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs new file mode 100644 index 0000000000..d1ab762410 --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.GethTest do + use ExUnit.Case, async: false + + doctest EthereumJSONRPC.Geth +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs index c67b8cf2f3..0c9b823823 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs @@ -1,9 +1,8 @@ defmodule EthereumJSONRPC.ParityTest do use ExUnit.Case, async: true - doctest EthereumJSONRPC.Parity - describe "fetch_internal_transactions/1" do + @tag :no_geth test "with all valid transaction_params returns {:ok, transactions_params}" do assert EthereumJSONRPC.Parity.fetch_internal_transactions([ %{ @@ -33,6 +32,7 @@ defmodule EthereumJSONRPC.ParityTest do } end + @tag :no_geth test "with all invalid transaction_params returns {:error, reasons}" do assert EthereumJSONRPC.Parity.fetch_internal_transactions([ %{ @@ -53,6 +53,7 @@ defmodule EthereumJSONRPC.ParityTest do ]} end + @tag :no_geth test "with a mix of valid and invalid transaction_params returns {:error, reasons}" do assert EthereumJSONRPC.Parity.fetch_internal_transactions([ # start with :ok diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs index fab742b008..5c4db94cbc 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs @@ -3,6 +3,10 @@ defmodule EthereumJSONRPC.ReceiptsTest do alias EthereumJSONRPC.Receipts + setup do + %{variant: EthereumJSONRPC.config(:variant)} + end + doctest Receipts # These are integration tests that depend on the sokol chain being used. sokol can be used with the following config @@ -12,32 +16,65 @@ defmodule EthereumJSONRPC.ReceiptsTest do # url: "https://sokol.poa.network" # describe "fetch/1" do - test "with receipts and logs" do - assert {:ok, - %{ - logs: [ + test "with receipts and logs", %{variant: variant} do + case variant do + EthereumJSONRPC.Geth -> + assert {:ok, %{ - address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", - data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", - first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22", - fourth_topic: nil, - index: 0, - second_topic: nil, - third_topic: nil, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" - } - ], - receipts: [ + logs: [], + receipts: [ + %{ + cumulative_gas_used: 1_238_877, + gas_used: 21000, + status: :ok, + transaction_hash: "0x360fb62cc817093e5624468735803ea39cad719e5c68ca322bae6ba4f520756f", + transaction_index: 57 + } + ] + }} = + Receipts.fetch([ + %{ + gas: 90000, + hash: "0x360fb62cc817093e5624468735803ea39cad719e5c68ca322bae6ba4f520756f" + } + ]) + + EthereumJSONRPC.Parity -> + assert {:ok, %{ - cumulative_gas_used: 50450, - gas_used: 50450, - status: :ok, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - transaction_index: 0 - } - ] - }} = Receipts.fetch(["0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5"]) + logs: [ + %{ + address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", + first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22", + fourth_topic: nil, + index: 0, + second_topic: nil, + third_topic: nil, + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", + type: "mined" + } + ], + receipts: [ + %{ + cumulative_gas_used: 50450, + gas_used: 50450, + status: :ok, + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", + transaction_index: 0 + } + ] + }} = + Receipts.fetch([ + %{ + gas: 50451, + hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + } + ]) + + _ -> + raise ArgumentError, "Unsupported variant (#{variant})" + end end end end diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex new file mode 100644 index 0000000000..efe651b366 --- /dev/null +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex @@ -0,0 +1,10 @@ +defmodule EthereumJSONRPC.Case do + require Logger + + def log_bad_gateway(under_test, assertions) do + case under_test.() do + {:error, {:bad_gateway, url}} -> Logger.error(fn -> ["Bad Gateway to ", url, ". Check CloudFlare."] end) + other -> assertions.(other) + end + end +end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 0ac1f2bcd7..5a16e4b389 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -178,7 +178,7 @@ defmodule Explorer.Chain do """ @spec address_to_transactions(Address.t(), [paging_options | necessity_by_association_option]) :: [Transaction.t()] def address_to_transactions( - %Address{hash: %Hash{byte_count: unquote(Hash.Truncated.byte_count())} = address_hash}, + %Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}, options \\ [] ) when is_list(options) do @@ -356,7 +356,7 @@ defmodule Explorer.Chain do [ [{:addresses, [timeout_option]}] | timeout_option ] - ) :: {:ok, [Hash.Truncated.t()]} | {:error, [Changeset.t()]} + ) :: {:ok, [Hash.Address.t()]} | {:error, [Changeset.t()]} def update_balances(addresses_params, options \\ []) when is_list(options) do with {:ok, changes_list} <- changes_list(addresses_params, for: Address, with: :balance_changeset) do timestamps = timestamps() @@ -504,12 +504,12 @@ defmodule Explorer.Chain do ...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0"} ...> ) ...> errors - [hash: {"is invalid", [type: Explorer.Chain.Hash.Truncated, validation: :cast]}] + [hash: {"is invalid", [type: Explorer.Chain.Hash.Address, validation: :cast]}] iex> {:error, %Ecto.Changeset{errors: errors}} = Explorer.Chain.create_address( ...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0ba"} ...> ) ...> errors - [hash: {"is invalid", [type: Explorer.Chain.Hash.Truncated, validation: :cast]}] + [hash: {"is invalid", [type: Explorer.Chain.Hash.Address, validation: :cast]}] """ @spec create_address(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} @@ -618,8 +618,8 @@ defmodule Explorer.Chain do {:error, :not_found} """ - @spec hash_to_address(Hash.Truncated.t()) :: {:ok, Address.t()} | {:error, :not_found} - def hash_to_address(%Hash{byte_count: unquote(Hash.Truncated.byte_count())} = hash) do + @spec hash_to_address(Hash.Address.t()) :: {:ok, Address.t()} | {:error, :not_found} + def hash_to_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do query = from( address in Address, @@ -635,7 +635,7 @@ defmodule Explorer.Chain do end end - def find_contract_address(%Hash{byte_count: unquote(Hash.Truncated.byte_count())} = hash) do + def find_contract_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do query = from( address in Address, @@ -804,10 +804,8 @@ defmodule Explorer.Chain do ...> index: 0, ...> input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", ...> nonce: 4, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: 0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01, ...> s: 0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f, - ...> standard_v: 1, ...> status: :ok, ...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", ...> v: 0xbe, @@ -965,10 +963,8 @@ defmodule Explorer.Chain do ...> index: 0, ...> input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", ...> nonce: 4, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: 0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01, ...> s: 0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f, - ...> standard_v: 1, ...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", ...> v: 0xbe, ...> value: 0 @@ -1182,7 +1178,7 @@ defmodule Explorer.Chain do ]) :: {:ok, %{ - optional(:addresses) => [Hash.Truncated.t()], + optional(:addresses) => [Hash.Address.t()], optional(:blocks) => [Hash.Full.t()], optional(:internal_transactions) => [ %{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()} @@ -1233,7 +1229,7 @@ defmodule Explorer.Chain do ]) :: {:ok, %{ - optional(:addresses) => [Hash.Truncated.t()], + optional(:addresses) => [Hash.Address.t()], optional(:internal_transactions) => [ %{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()} ] @@ -1634,7 +1630,7 @@ defmodule Explorer.Chain do @spec stream_unfetched_addresses( initial :: accumulator, reducer :: - (entry :: %{block_number: Block.block_number(), hash: Hash.Truncated.t()}, accumulator -> accumulator) + (entry :: %{block_number: Block.block_number(), hash: Hash.Address.t()}, accumulator -> accumulator) ) :: {:ok, accumulator} when accumulator: term() def stream_unfetched_addresses(initial, reducer) when is_function(reducer, 2) do @@ -1713,10 +1709,8 @@ defmodule Explorer.Chain do | :index | :input | :nonce - | :public_key | :r | :s - | :standard_v | :to_address_hash | :v | :value @@ -1991,7 +1985,7 @@ defmodule Explorer.Chain do end @doc """ - The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.Truncated.t/0`. + The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.Address.t/0`. iex> Explorer.Chain.string_to_address_hash("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") { @@ -2008,9 +2002,9 @@ defmodule Explorer.Chain do :error """ - @spec string_to_address_hash(String.t()) :: {:ok, Hash.Truncated.t()} | :error + @spec string_to_address_hash(String.t()) :: {:ok, Hash.Address.t()} | :error def string_to_address_hash(string) when is_binary(string) do - Hash.Truncated.cast(string) + Hash.Address.cast(string) end @doc """ @@ -2274,8 +2268,7 @@ defmodule Explorer.Chain do ) end - @spec insert_addresses([%{hash: Hash.Truncated.t()}], [timeout_option | timestamps_option]) :: - {:ok, [Hash.Truncated.t()]} + @spec insert_addresses([%{hash: Hash.Address.t()}], [timeout_option | timestamps_option]) :: {:ok, [Hash.Address.t()]} defp insert_addresses(changes_list, named_arguments) when is_list(changes_list) and is_list(named_arguments) do timestamps = Keyword.fetch!(named_arguments, :timestamps) diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 7be4f797f9..5b4de69c5c 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -29,13 +29,13 @@ defmodule Explorer.Chain.Address do @type t :: %__MODULE__{ fetched_balance: Wei.t(), fetched_balance_block_number: Block.block_number(), - hash: Hash.Truncated.t(), + hash: Hash.Address.t(), contract_code: Data.t() | nil, inserted_at: DateTime.t(), updated_at: DateTime.t() } - @primary_key {:hash, Hash.Truncated, autogenerate: false} + @primary_key {:hash, Hash.Address, autogenerate: false} schema "addresses" do field(:fetched_balance, Wei) field(:fetched_balance_block_number, :integer) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 058b6e8650..f458bf8a5c 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -46,10 +46,10 @@ defmodule Explorer.Chain.Block do difficulty: difficulty(), gas_limit: Gas.t(), gas_used: Gas.t(), - hash: Hash.t(), + hash: Hash.Full.t(), miner: %Ecto.Association.NotLoaded{} | Address.t(), - miner_hash: Hash.Truncated.t(), - nonce: Hash.t(), + miner_hash: Hash.Address.t(), + nonce: Hash.Nonce.t(), number: block_number(), parent_hash: Hash.t(), size: non_neg_integer(), @@ -63,7 +63,7 @@ defmodule Explorer.Chain.Block do field(:difficulty, :decimal) field(:gas_limit, :integer) field(:gas_used, :integer) - field(:nonce, :integer) + field(:nonce, Hash.Nonce) field(:number, :integer) field(:size, :integer) field(:timestamp, :utc_datetime) @@ -71,7 +71,7 @@ defmodule Explorer.Chain.Block do timestamps() - belongs_to(:miner, Address, foreign_key: :miner_hash, references: :hash, type: Hash.Truncated) + belongs_to(:miner, Address, foreign_key: :miner_hash, references: :hash, type: Hash.Address) belongs_to(:parent, __MODULE__, foreign_key: :parent_hash, references: :hash, type: Hash.Full) has_many(:transactions, Transaction) end diff --git a/apps/explorer/lib/explorer/chain/data.ex b/apps/explorer/lib/explorer/chain/data.ex index fafa6058aa..f70f906ab0 100644 --- a/apps/explorer/lib/explorer/chain/data.ex +++ b/apps/explorer/lib/explorer/chain/data.ex @@ -81,7 +81,7 @@ defmodule Explorer.Chain.Data do {:ok, %Explorer.Chain.Data{bytes: <<>>}} Hashes can be represented as `Explorer.Chain.Data`, but it is better to use `Explorer.Chain.Hash.Full` or - `Explorer.Chain.Hash.Truncated`. + `Explorer.Chain.Hash.Address`. iex> Explorer.Chain.Data.cast("0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b") { diff --git a/apps/explorer/lib/explorer/chain/hash/truncated.ex b/apps/explorer/lib/explorer/chain/hash/address.ex similarity index 86% rename from apps/explorer/lib/explorer/chain/hash/truncated.ex rename to apps/explorer/lib/explorer/chain/hash/address.ex index e3fa59c1f1..e0fd8b4e77 100644 --- a/apps/explorer/lib/explorer/chain/hash/truncated.ex +++ b/apps/explorer/lib/explorer/chain/hash/address.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Chain.Hash.Truncated do +defmodule Explorer.Chain.Hash.Address do @moduledoc """ The address (40 (hex) characters / 160 bits / 20 bytes) is derived from the public key (128 (hex) characters / 512 bits / 64 bytes) which is derived from the private key (64 (hex) characters / 256 bits / 32 bytes). @@ -24,7 +24,7 @@ defmodule Explorer.Chain.Hash.Truncated do If the `term` is already in `t:t/0`, then it is returned - iex> Explorer.Chain.Hash.Truncated.cast( + iex> Explorer.Chain.Hash.Address.cast( ...> %Explorer.Chain.Hash{ ...> byte_count: 20, ...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>> @@ -40,7 +40,7 @@ defmodule Explorer.Chain.Hash.Truncated do If the `term` is an `non_neg_integer`, then it is converted to `t:t/0` - iex> Explorer.Chain.Hash.Truncated.cast(0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed) + iex> Explorer.Chain.Hash.Address.cast(0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed) { :ok, %Explorer.Chain.Hash{ @@ -51,12 +51,12 @@ defmodule Explorer.Chain.Hash.Truncated do If the `non_neg_integer` is too large, then `:error` is returned. - iex> Explorer.Chain.Hash.Truncated.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b) + iex> Explorer.Chain.Hash.Address.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b) :error If the `term` is a `String.t` that starts with `0x`, then is converted to an integer and then to `t:t/0`. - iex> Explorer.Chain.Hash.Truncated.cast("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") + iex> Explorer.Chain.Hash.Address.cast("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") { :ok, %Explorer.Chain.Hash{ @@ -68,7 +68,7 @@ defmodule Explorer.Chain.Hash.Truncated do While `non_neg_integers` don't have to be the correct width (because zero padding it difficult with numbers), `String.t` format must always have #{@hexadecimal_digit_count} digits after the `0x` base prefix. - iex> Explorer.Chain.Hash.Truncated.cast("0x0") + iex> Explorer.Chain.Hash.Address.cast("0x0") :error """ @@ -83,7 +83,7 @@ defmodule Explorer.Chain.Hash.Truncated do If the field from the struct is `t:t/0`, then it succeeds - iex> Explorer.Chain.Hash.Truncated.dump( + iex> Explorer.Chain.Hash.Address.dump( ...> %Explorer.Chain.Hash{ ...> byte_count: 20, ...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>> @@ -93,7 +93,7 @@ defmodule Explorer.Chain.Hash.Truncated do If the field from the struct is an incorrect format such as `t:Explorer.Chain.Hash.t/0`, `:error` is returned - iex> Explorer.Chain.Hash.Truncated.dump( + iex> Explorer.Chain.Hash.Address.dump( ...> %Explorer.Chain.Hash{ ...> byte_count: 32, ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: @@ -114,7 +114,7 @@ defmodule Explorer.Chain.Hash.Truncated do If the binary hash is the correct format, it is returned. - iex> Explorer.Chain.Hash.Truncated.load( + iex> Explorer.Chain.Hash.Address.load( ...> <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>> ...> ) { @@ -127,7 +127,7 @@ defmodule Explorer.Chain.Hash.Truncated do If the binary hash is an incorrect format, such as if an `Explorer.Chain.Hash` field is loaded, `:error` is returned. - iex> Explorer.Chain.Hash.Truncated.load( + iex> Explorer.Chain.Hash.Address.load( ...> <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> ...> ) :error diff --git a/apps/explorer/lib/explorer/chain/hash/nonce.ex b/apps/explorer/lib/explorer/chain/hash/nonce.ex new file mode 100644 index 0000000000..e5dbf3fe73 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/hash/nonce.ex @@ -0,0 +1,146 @@ +defmodule Explorer.Chain.Hash.Nonce do + @moduledoc """ + The nonce (16 (hex) characters / 128 bits / 8 bytes) is derived from the Proof-of-Work. + """ + + alias Explorer.Chain.Hash + + @behaviour Ecto.Type + @behaviour Hash + + @byte_count 8 + @hexadecimal_digit_count Hash.hexadecimal_digits_per_byte() * @byte_count + + @typedoc """ + A #{@byte_count}-byte hash of the address public key. + """ + @type t :: %Hash{byte_count: unquote(@byte_count), bytes: <<_::unquote(@byte_count * Hash.bits_per_byte())>>} + + @doc """ + Casts `term` to `t:t/0`. + + If the `term` is already in `t:t/0`, then it is returned + + iex> Explorer.Chain.Hash.Nonce.cast( + ...> %Explorer.Chain.Hash{ + ...> byte_count: 8, + ...> bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> + ...> } + ...> ) + { + :ok, + %Explorer.Chain.Hash{ + byte_count: 8, + bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> + } + } + + If the `term` is an `non_neg_integer`, then it is converted to `t:t/0` + + iex> Explorer.Chain.Hash.Nonce.cast(0x7bb9369dcbaec019) + { + :ok, + %Explorer.Chain.Hash{ + byte_count: 8, + bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> + } + } + + If the `non_neg_integer` is too large, then `:error` is returned. + + iex> Explorer.Chain.Hash.Nonce.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b) + :error + + If the `term` is a `String.t` that starts with `0x`, then is converted to an integer and then to `t:t/0`. + + iex> Explorer.Chain.Hash.Nonce.cast("0x7bb9369dcbaec019") + { + :ok, + %Explorer.Chain.Hash{ + byte_count: 8, + bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> + } + } + + While `non_neg_integers` don't have to be the correct width (because zero padding it difficult with numbers), + `String.t` format must always have #{@hexadecimal_digit_count} digits after the `0x` base prefix. + + iex> Explorer.Chain.Hash.Address.cast("0x0") + :error + + """ + @impl Ecto.Type + @spec cast(term()) :: {:ok, t()} | :error + def cast(term) do + Hash.cast(__MODULE__, term) + end + + @doc """ + Dumps the binary hash to `:binary` (`bytea`) format used in database. + + If the field from the struct is `t:t/0`, then it succeeds + + iex> Explorer.Chain.Hash.Nonce.dump( + ...> %Explorer.Chain.Hash{ + ...> byte_count: 8, + ...> bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> + ...> } + ...> ) + {:ok, <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>>} + + If the field from the struct is an incorrect format such as `t:Explorer.Chain.Hash.t/0`, `:error` is returned + + iex> Explorer.Chain.Hash.Nonce.dump( + ...> %Explorer.Chain.Hash{ + ...> byte_count: 32, + ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: + ...> big-integer-size(32)-unit(8)>> + ...> } + ...> ) + :error + + """ + @impl Ecto.Type + @spec dump(term()) :: {:ok, binary} | :error + def dump(term) do + Hash.dump(__MODULE__, term) + end + + @doc """ + Loads the binary hash from the database. + + If the binary hash is the correct format, it is returned. + + iex> Explorer.Chain.Hash.Nonce.load(<<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>>) + { + :ok, + %Explorer.Chain.Hash{ + byte_count: 8, + bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> + } + } + + If the binary hash is an incorrect format, such as if an `Explorer.Chain.Hash` field is loaded, `:error` is returned. + + iex> Explorer.Chain.Hash.Nonce.load( + ...> <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> + ...> ) + :error + + """ + @impl Ecto.Type + @spec load(term()) :: {:ok, t} | :error + def load(term) do + Hash.load(__MODULE__, term) + end + + @doc """ + The underlying database type: `binary`. `binary` is used because no Postgres integer type is 20 bytes long. + """ + @impl Ecto.Type + @spec type() :: :binary + def type, do: :binary + + @impl Hash + def byte_count, do: @byte_count +end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index a93cb0526c..c4b059ddcf 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -33,7 +33,7 @@ defmodule Explorer.Chain.InternalTransaction do created_contract_code: Data.t() | nil, error: String.t(), from_address: %Ecto.Association.NotLoaded{} | Address.t(), - from_address_hash: Hash.Truncated.t(), + from_address_hash: Hash.Address.t(), gas: Gas.t(), gas_used: Gas.t() | nil, index: non_neg_integer(), @@ -41,7 +41,7 @@ defmodule Explorer.Chain.InternalTransaction do input: Data.t(), output: Data.t() | nil, to_address: %Ecto.Association.NotLoaded{} | Address.t(), - to_address_hash: Hash.Truncated.t(), + to_address_hash: Hash.Address.t(), trace_address: [non_neg_integer()], transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), transaction_hash: Explorer.Chain.Hash.t(), @@ -70,7 +70,7 @@ defmodule Explorer.Chain.InternalTransaction do Address, foreign_key: :created_contract_address_hash, references: :hash, - type: Hash.Truncated + type: Hash.Address ) belongs_to( @@ -78,7 +78,7 @@ defmodule Explorer.Chain.InternalTransaction do Address, foreign_key: :from_address_hash, references: :hash, - type: Hash.Truncated + type: Hash.Address ) belongs_to( @@ -86,7 +86,7 @@ defmodule Explorer.Chain.InternalTransaction do Address, foreign_key: :to_address_hash, references: :hash, - type: Hash.Truncated + type: Hash.Address ) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full) diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 91101a16b6..f61e8f4e37 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -5,8 +5,8 @@ defmodule Explorer.Chain.Log do alias Explorer.Chain.{Address, Data, Hash, Transaction} - @required_attrs ~w(address_hash data index transaction_hash type)a - @optional_attrs ~w(first_topic second_topic third_topic fourth_topic)a + @required_attrs ~w(address_hash data index transaction_hash)a + @optional_attrs ~w(first_topic second_topic third_topic fourth_topic type)a @typedoc """ * `address` - address of contract that generate the event @@ -19,11 +19,11 @@ defmodule Explorer.Chain.Log do * `transaction` - transaction for which `log` is * `transaction_hash` - foreign key for `transaction`. * `third_topic` - `topics[2]` - * `type` - type of event + * `type` - type of event. *Parity-only* """ @type t :: %__MODULE__{ address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Truncated.t(), + address_hash: Hash.Address.t(), data: Data.t(), first_topic: String.t(), fourth_topic: String.t(), @@ -32,7 +32,7 @@ defmodule Explorer.Chain.Log do transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), transaction_hash: Hash.Full.t(), third_topic: String.t(), - type: String.t() + type: String.t() | nil } schema "logs" do @@ -46,7 +46,7 @@ defmodule Explorer.Chain.Log do timestamps() - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Truncated) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full) end diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index a64cecef27..dcc089dc27 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -32,7 +32,7 @@ defmodule Explorer.Chain.SmartContract do Address, foreign_key: :address_hash, references: :hash, - type: Hash.Truncated + type: Hash.Address ) timestamps() diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index d50464333f..ccc658596f 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -9,12 +9,7 @@ defmodule Explorer.Chain.Transaction do @optional_attrs ~w(block_hash block_number cumulative_gas_used from_address_hash gas_used index internal_transactions_indexed_at status to_address_hash)a - @required_attrs ~w(gas gas_price hash input nonce public_key r s standard_v v value)a - - @typedoc """ - The full public key of the signer of the transaction. - """ - @type public_key :: Data.t() + @required_attrs ~w(gas gas_price hash input nonce r s v value)a @typedoc """ X coordinate module n in @@ -30,28 +25,6 @@ defmodule Explorer.Chain.Transaction do """ @type s :: Decimal.t() - @typedoc """ - For message signatures, we use a trick called public key recovery. The fact is that if you have the full R point - (not just its X coordinate) and `t:s/0`, and a message, you can compute for which public key this would be a valid - signature. What this allows is to 'verify' a message with an address, without needing to know the full key (we just to - public key recovery on the signature, and then hash the recovered key and compare it with the address). - - However, this means we need the full R coordinates. There can be up to 4 different points with a given - "X coordinate modulo n". (2 because each X coordinate has two possible Y coordinates, and 2 because r+n may still be a - valid X coordinate). That number between 0 and 3 is standard_v. - - | `standard_v` | X | Y | - |---------------|--------|------| - | `0` | lower | even | - | `1` | lower | odd | - | `2` | higher | even | - | `3` | higher | odd | - - **Note: that `2` and `3` are exceedingly rarely, and will in practice only ever be seen in specifically generated - examples.** - """ - @type standard_v :: 0..3 - @typedoc """ The index of the transaction in its block. """ @@ -96,12 +69,10 @@ defmodule Explorer.Chain.Transaction do * `internal_transactions_indexed_at` - when `internal_transactions` were fetched by `Explorer.Indexer`. * `logs` - events that occurred while mining the `transaction`. * `nonce` - the number of transaction made by the sender prior to this one - * `public_key` - public key of the signer of the transaction * `r` - the R field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as the X coordinate of a point R, modulo the curve order n. * `s` - The S field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as the X coordinate of a point R, modulo the curve order n. - * `standard_v` - The standardized V field of the signature * `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending. * `to_address` - sink of `value` * `to_address_hash` - `to_address` foreign key @@ -114,7 +85,7 @@ defmodule Explorer.Chain.Transaction do block_number: Block.block_number() | nil, cumulative_gas_used: Gas.t() | nil, from_address: %Ecto.Association.NotLoaded{} | Address.t(), - from_address_hash: Hash.Truncated.t(), + from_address_hash: Hash.Address.t(), gas: Gas.t(), gas_price: wei_per_gas, gas_used: Gas.t() | nil, @@ -125,13 +96,11 @@ defmodule Explorer.Chain.Transaction do internal_transactions_indexed_at: DateTime.t(), logs: %Ecto.Association.NotLoaded{} | [Log.t()], nonce: non_neg_integer(), - public_key: public_key(), r: r(), s: s(), - standard_v: standard_v(), status: Status.t() | nil, to_address: %Ecto.Association.NotLoaded{} | Address.t(), - to_address_hash: Hash.Truncated.t(), + to_address_hash: Hash.Address.t(), v: v(), value: Wei.t() } @@ -147,14 +116,12 @@ defmodule Explorer.Chain.Transaction do field(:internal_transactions_indexed_at, :utc_datetime) field(:input, Data) field(:nonce, :integer) - field(:public_key, Data) field(:r, :decimal) field(:s, :decimal) - field(:standard_v, :integer) field(:status, Status) field(:v, :integer) field(:value, Wei) - field(:created_contract_address_hash, Hash.Truncated, virtual: true) + field(:created_contract_address_hash, Hash.Address, virtual: true) timestamps() @@ -165,7 +132,7 @@ defmodule Explorer.Chain.Transaction do Address, foreign_key: :from_address_hash, references: :hash, - type: Hash.Truncated + type: Hash.Address ) has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash) @@ -176,7 +143,7 @@ defmodule Explorer.Chain.Transaction do Address, foreign_key: :to_address_hash, references: :hash, - type: Hash.Truncated + type: Hash.Address ) end @@ -191,10 +158,8 @@ defmodule Explorer.Chain.Transaction do ...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> nonce: 0, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, - ...> standard_v: 0x0, ...> v: 0x8d, ...> value: 0 ...> } @@ -217,10 +182,8 @@ defmodule Explorer.Chain.Transaction do ...> index: 0, ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> nonce: 0, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, - ...> standard_v: 0x0, ...> status: :ok, ...> v: 0x8d, ...> value: 0 @@ -254,10 +217,8 @@ defmodule Explorer.Chain.Transaction do ...> index: 0, ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> nonce: 0, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, - ...> standard_v: 0x0, ...> status: :ok, ...> v: 0x8d, ...> value: 0 @@ -278,10 +239,8 @@ defmodule Explorer.Chain.Transaction do ...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> nonce: 0, - ...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, - ...> standard_v: 0x0, ...> v: 0x8d, ...> value: 0 ...> } @@ -305,7 +264,6 @@ defmodule Explorer.Chain.Transaction do |> cast(attrs, @required_attrs ++ @optional_attrs) |> validate_required(@required_attrs) |> validate_collated_or_pending() - |> validate_number(:standard_v, greater_than_or_equal_to: 0, less_than_or_equal_to: 3) |> check_pending() |> check_collated() |> foreign_key_constraint(:block_hash) diff --git a/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs b/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs index f3047bd095..3e97210bd8 100644 --- a/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs +++ b/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs @@ -8,7 +8,7 @@ defmodule Explorer.Repo.Migrations.CreateBlocks do add(:gas_used, :integer, null: false) add(:hash, :bytea, null: false, primary_key: true) add(:miner_hash, references(:addresses, column: :hash, type: :bytea), null: false) - add(:nonce, :integer, null: false) + add(:nonce, :bytea, null: false) add(:number, :bigint, null: false) # not a foreign key to allow skipped blocks diff --git a/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs b/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs index 200315b8e3..1f07aa357c 100644 --- a/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs +++ b/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs @@ -23,10 +23,8 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do add(:internal_transactions_indexed_at, :utc_datetime, null: true) add(:nonce, :integer, null: false) - add(:public_key, :bytea, null: false) add(:r, :numeric, precision: 100, null: false) add(:s, :numeric, precision: 100, null: false) - add(:standard_v, :smallint, null: false) # `null` when a pending transaction add(:status, :integer, null: true) @@ -128,8 +126,6 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do ) ) - create(constraint(:transactions, :standard_v, check: "0 <= standard_v AND standard_v <= 3")) - create(index(:transactions, :block_hash)) create(index(:transactions, :from_address_hash)) create(index(:transactions, :to_address_hash)) diff --git a/apps/explorer/priv/repo/migrations/20180212222309_create_logs.exs b/apps/explorer/priv/repo/migrations/20180212222309_create_logs.exs index d3458b38d4..738ec2c786 100644 --- a/apps/explorer/priv/repo/migrations/20180212222309_create_logs.exs +++ b/apps/explorer/priv/repo/migrations/20180212222309_create_logs.exs @@ -5,7 +5,10 @@ defmodule Explorer.Repo.Migrations.CreateLogs do create table(:logs) do add(:data, :bytea, null: false) add(:index, :integer, null: false) - add(:type, :string, null: false) + + # Parity supplies it; Geth does not. + add(:type, :string, null: true) + add(:first_topic, :string, null: true) add(:second_topic, :string, null: true) add(:third_topic, :string, null: true) diff --git a/apps/explorer/test/explorer/chain/hash/address_test.exs b/apps/explorer/test/explorer/chain/hash/address_test.exs new file mode 100644 index 0000000000..4ca2206315 --- /dev/null +++ b/apps/explorer/test/explorer/chain/hash/address_test.exs @@ -0,0 +1,5 @@ +defmodule Explorer.Chain.Hash.AddressTest do + use ExUnit.Case, async: true + + doctest Explorer.Chain.Hash.Address +end diff --git a/apps/explorer/test/explorer/chain/hash/nonce_test.exs b/apps/explorer/test/explorer/chain/hash/nonce_test.exs new file mode 100644 index 0000000000..31793c3d90 --- /dev/null +++ b/apps/explorer/test/explorer/chain/hash/nonce_test.exs @@ -0,0 +1,5 @@ +defmodule Explorer.Chain.Hash.NonceTest do + use ExUnit.Case, async: true + + doctest Explorer.Chain.Hash.Nonce +end diff --git a/apps/explorer/test/explorer/chain/hash/truncated_test.exs b/apps/explorer/test/explorer/chain/hash/truncated_test.exs deleted file mode 100644 index 4cfac3b5b7..0000000000 --- a/apps/explorer/test/explorer/chain/hash/truncated_test.exs +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Explorer.Chain.Hash.TruncatedTest do - use ExUnit.Case, async: true - - doctest Explorer.Chain.Hash.Truncated -end diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index 978f546aa9..f178d8ffe9 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -16,10 +16,8 @@ defmodule Explorer.Chain.TransactionTest do gas_price: 10000, input: "0x5c8eff12", nonce: "31337", - public_key: "0xb39af9cb", r: 0x9, s: 0x10, - standard_v: 0x1, transaction_index: "0x12", v: 27 }) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index a5b20cb046..018264f964 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -65,7 +65,7 @@ defmodule Explorer.Factory do {:ok, address_hash} = "address_hash" |> sequence(& &1) - |> Hash.Truncated.cast() + |> Hash.Address.cast() address_hash end @@ -217,10 +217,6 @@ defmodule Explorer.Factory do } end - def public_key do - data(:public_key) - end - def market_history_factory do %MarketHistory{ closing_price: price(), @@ -258,10 +254,8 @@ defmodule Explorer.Factory do hash: transaction_hash(), input: transaction_input(), nonce: Enum.random(1..1_000), - public_key: public_key(), r: sequence(:transaction_r, & &1), s: sequence(:transaction_s, & &1), - standard_v: Enum.random(0..3), to_address: build(:address), v: Enum.random(27..30), value: Enum.random(1..100_000) diff --git a/apps/indexer/.formatter.exs b/apps/indexer/.formatter.exs deleted file mode 100644 index 525446d406..0000000000 --- a/apps/indexer/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/indexer/lib/indexer/address_balance_fetcher.ex b/apps/indexer/lib/indexer/address_balance_fetcher.ex index 751fb46292..ccd6efe0d9 100644 --- a/apps/indexer/lib/indexer/address_balance_fetcher.ex +++ b/apps/indexer/lib/indexer/address_balance_fetcher.ex @@ -22,7 +22,7 @@ defmodule Indexer.AddressBalanceFetcher do @doc """ Asynchronously fetches balances for each address `hash` at the `block_number`. """ - @spec async_fetch_balances([%{required(:block_number) => Block.block_number(), required(:hash) => Hash.Truncated.t()}]) :: + @spec async_fetch_balances([%{required(:block_number) => Block.block_number(), required(:hash) => Hash.Address.t()}]) :: :ok def async_fetch_balances(address_fields) when is_list(address_fields) do params_list = Enum.map(address_fields, &address_fields_to_params/1) diff --git a/apps/indexer/lib/indexer/block_fetcher.ex b/apps/indexer/lib/indexer/block_fetcher.ex index 8da22ee3bf..ab8338ee41 100644 --- a/apps/indexer/lib/indexer/block_fetcher.ex +++ b/apps/indexer/lib/indexer/block_fetcher.ex @@ -10,7 +10,6 @@ defmodule Indexer.BlockFetcher do import Indexer, only: [debug: 1] alias EthereumJSONRPC - alias EthereumJSONRPC.Transactions alias Explorer.Chain alias Indexer.{AddressBalanceFetcher, AddressExtraction, BufferedTask, InternalTransactionFetcher, Sequence} @@ -163,11 +162,11 @@ defmodule Indexer.BlockFetcher do defp fetch_transaction_receipts(_state, []), do: {:ok, %{logs: [], receipts: []}} - defp fetch_transaction_receipts(%{} = state, hashes) do - debug(fn -> "fetching #{length(hashes)} transaction receipts" end) + defp fetch_transaction_receipts(%{} = state, transaction_params) do + debug(fn -> "fetching #{length(transaction_params)} transaction receipts" end) stream_opts = [max_concurrency: state.receipts_concurrency, timeout: :infinity] - hashes + transaction_params |> Enum.chunk_every(state.receipts_batch_size) |> Task.async_stream(&EthereumJSONRPC.fetch_transaction_receipts(&1), stream_opts) |> Enum.reduce_while({:ok, %{logs: [], receipts: []}}, fn @@ -316,8 +315,8 @@ defmodule Indexer.BlockFetcher do with {:blocks, {:ok, next, result}} <- {:blocks, EthereumJSONRPC.fetch_blocks_by_range(range)}, %{blocks: blocks, transactions: transactions_without_receipts} = result, cap_seq(seq, next, range), - transaction_hashes = Transactions.params_to_hashes(transactions_without_receipts), - {:receipts, {:ok, receipt_params}} <- {:receipts, fetch_transaction_receipts(state, transaction_hashes)}, + {:receipts, {:ok, receipt_params}} <- + {:receipts, fetch_transaction_receipts(state, transactions_without_receipts)}, %{logs: logs, receipts: receipts} = receipt_params, transactions_with_receipts = put_receipts(transactions_without_receipts, receipts) do addresses = diff --git a/apps/indexer/lib/indexer/internal_transaction_fetcher.ex b/apps/indexer/lib/indexer/internal_transaction_fetcher.ex index 233f466ba4..4914bd5eec 100644 --- a/apps/indexer/lib/indexer/internal_transaction_fetcher.ex +++ b/apps/indexer/lib/indexer/internal_transaction_fetcher.ex @@ -125,6 +125,9 @@ defmodule Indexer.InternalTransactionFetcher do # re-queue the de-duped transactions_params {:retry, unique_transactions_params} + + :ignore -> + :ok end end diff --git a/apps/indexer/lib/indexer/pending_transaction_fetcher.ex b/apps/indexer/lib/indexer/pending_transaction_fetcher.ex index ba8b808f38..1a698b84be 100644 --- a/apps/indexer/lib/indexer/pending_transaction_fetcher.ex +++ b/apps/indexer/lib/indexer/pending_transaction_fetcher.ex @@ -9,7 +9,7 @@ defmodule Indexer.PendingTransactionFetcher do require Logger - import EthereumJSONRPC.Parity, only: [fetch_pending_transactions: 0] + import EthereumJSONRPC, only: [fetch_pending_transactions: 0] alias Explorer.Chain alias Indexer.{AddressExtraction, PendingTransactionFetcher} @@ -85,17 +85,21 @@ defmodule Indexer.PendingTransactionFetcher do end defp task(%PendingTransactionFetcher{} = _state) do - {:ok, transactions_params} = fetch_pending_transactions() - - addresses_params = AddressExtraction.extract_addresses(%{transactions: transactions_params}, pending: true) - - # There's no need to queue up fetching the address balance since theses are pending transactions and cannot have - # affected the address balance yet since address balance is a balance at a give block and these transactions are - # blockless. - {:ok, _} = - Chain.import_blocks( - addresses: [params: addresses_params], - transactions: [on_conflict: :nothing, params: transactions_params] - ) + case fetch_pending_transactions() do + {:ok, transactions_params} -> + addresses_params = AddressExtraction.extract_addresses(%{transactions: transactions_params}, pending: true) + + # There's no need to queue up fetching the address balance since theses are pending transactions and cannot have + # affected the address balance yet since address balance is a balance at a give block and these transactions are + # blockless. + {:ok, _} = + Chain.import_blocks( + addresses: [params: addresses_params], + transactions: [on_conflict: :nothing, params: transactions_params] + ) + + :ignore -> + :ok + end end end diff --git a/apps/indexer/test/indexer/address_balance_fetcher_test.exs b/apps/indexer/test/indexer/address_balance_fetcher_test.exs index e54bc0544d..6145d59b29 100644 --- a/apps/indexer/test/indexer/address_balance_fetcher_test.exs +++ b/apps/indexer/test/indexer/address_balance_fetcher_test.exs @@ -6,23 +6,37 @@ defmodule Indexer.AddressBalanceFetcherTest do alias Explorer.Chain.{Address, Hash, Wei} alias Indexer.{AddressBalanceFetcher, AddressBalanceFetcherCase} - @block_number 2_932_838 - @hash %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>> - } - setup do start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) - :ok + %{variant: EthereumJSONRPC.config(:variant)} end describe "init/1" do - test "fetches unfetched Block miner balance" do - {:ok, miner_hash} = Hash.Truncated.cast("0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca") + test "fetches unfetched Block miner balance", %{variant: variant} do + %{block_number: block_number, fetched_balance: fetched_balance, miner_hash_data: miner_hash_data} = + case variant do + EthereumJSONRPC.Geth -> + %{ + block_number: 201_480, + fetched_balance: 6_301_752_965_671_077_173, + miner_hash_data: "0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca" + } + + EthereumJSONRPC.Parity -> + %{ + block_number: 34, + fetched_balance: 252_460_834_000_000_000_000_000_000, + miner_hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + } + + _ -> + raise ArgumentError, "Unsupported variant (#{variant})" + end + + {:ok, miner_hash} = Hash.Address.cast(miner_hash_data) miner = insert(:address, hash: miner_hash) - block = insert(:block, miner: miner, number: 34) + block = insert(:block, miner: miner, number: block_number) assert miner.fetched_balance == nil assert miner.fetched_balance_block_number == nil @@ -36,14 +50,34 @@ defmodule Indexer.AddressBalanceFetcherTest do ) end) - assert fetched_address.fetched_balance == %Wei{value: Decimal.new(252_460_834_000_000_000_000_000_000)} + assert fetched_address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)} assert fetched_address.fetched_balance_block_number == block.number end - test "fetches unfetched addresses when less than max batch size" do - {:ok, miner_hash} = Hash.Truncated.cast("0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca") + test "fetches unfetched addresses when less than max batch size", %{variant: variant} do + %{block_number: block_number, fetched_balance: fetched_balance, miner_hash_data: miner_hash_data} = + case variant do + EthereumJSONRPC.Geth -> + %{ + block_number: 201_480, + fetched_balance: 6_301_752_965_671_077_173, + miner_hash_data: "0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca" + } + + EthereumJSONRPC.Parity -> + %{ + block_number: 34, + fetched_balance: 252_460_834_000_000_000_000_000_000, + miner_hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + } + + _ -> + raise ArgumentError, "Unsupported variant (#{variant})" + end + + {:ok, miner_hash} = Hash.Address.cast(miner_hash_data) miner = insert(:address, hash: miner_hash) - block = insert(:block, miner: miner, number: 34) + block = insert(:block, miner: miner, number: block_number) AddressBalanceFetcherCase.start_supervised!(max_batch_size: 2) @@ -54,43 +88,93 @@ defmodule Indexer.AddressBalanceFetcherTest do ) end) - assert fetched_address.fetched_balance == %Wei{value: Decimal.new(252_460_834_000_000_000_000_000_000)} + assert fetched_address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)} assert fetched_address.fetched_balance_block_number == block.number end end describe "async_fetch_balances/1" do - test "fetches balances for address_hashes" do + test "fetches balances for address_hashes", %{variant: variant} do AddressBalanceFetcherCase.start_supervised!() - assert :ok = AddressBalanceFetcher.async_fetch_balances([%{block_number: @block_number, hash: @hash}]) + %{block_number: block_number, fetched_balance: fetched_balance, hash: hash} = + case variant do + EthereumJSONRPC.Geth -> + %{ + block_number: 201_480, + fetched_balance: 6_301_752_965_671_077_173, + hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<230, 167, 161, 212, 127, 242, 27, 99, 33, 22, 42, 234, 124, 108, 180, 87, 213, 71, 107, 202>> + } + } + + EthereumJSONRPC.Parity -> + %{ + block_number: 34, + fetched_balance: 252_460_834_000_000_000_000_000_000, + hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122, 202>> + } + } + + _ -> + raise ArgumentError, "Unsupported variant (#{variant})" + end + + assert :ok = AddressBalanceFetcher.async_fetch_balances([%{block_number: block_number, hash: hash}]) address = wait(fn -> - Repo.get!(Address, @hash) + Repo.get!(Address, hash) end) - assert address.fetched_balance == %Wei{value: Decimal.new(1)} - assert address.fetched_balance_block_number == @block_number + assert address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)} + assert address.fetched_balance_block_number == block_number end end describe "run/2" do - test "duplicate address hashes the max block_quantity" do - hash_data = "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" - - assert AddressBalanceFetcher.run( - [%{block_quantity: "0x1", hash_data: hash_data}, %{block_quantity: "0x2", hash_data: hash_data}], - 0 - ) == :ok - - fetched_address = Repo.one!(from(address in Address, where: address.hash == ^hash_data)) - - assert fetched_address.fetched_balance == %Explorer.Chain.Wei{ - value: Decimal.new(252_460_802_000_000_000_000_000_000) - } - - assert fetched_address.fetched_balance_block_number == 2 + @tag capture_log: true + test "duplicate address hashes the max block_quantity", %{variant: variant} do + %{fetched_balance: fetched_balance, hash_data: hash_data} = + case variant do + EthereumJSONRPC.Geth -> + %{ + fetched_balance: 5_000_000_000_000_000_000, + hash_data: "0x05a56e2d52c817161883f50c441c3228cfe54d9f" + } + + EthereumJSONRPC.Parity -> + %{ + fetched_balance: 252_460_802_000_000_000_000_000_000, + hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + } + + _ -> + raise ArgumentError, "Unsupported variant (#{variant})" + end + + case AddressBalanceFetcher.run( + [%{block_quantity: "0x1", hash_data: hash_data}, %{block_quantity: "0x2", hash_data: hash_data}], + 0 + ) do + :ok -> + fetched_address = Repo.one!(from(address in Address, where: address.hash == ^hash_data)) + + assert fetched_address.fetched_balance == %Explorer.Chain.Wei{ + value: Decimal.new(fetched_balance) + } + + assert fetched_address.fetched_balance_block_number == 2 + + other -> + # not all nodes behind the `https://mainnet.infura.io` pool are fully-synced. Node that aren't fully-synced + # won't have historical address balances. + assert {:retry, [%{block_quantity: "0x2", hash_data: ^hash_data}]} = other + end end test "duplicate address hashes only retry max block_quantity" do diff --git a/apps/indexer/test/indexer/block_fetcher_test.exs b/apps/indexer/test/indexer/block_fetcher_test.exs index 82cf0f4ec2..b290eab17b 100644 --- a/apps/indexer/test/indexer/block_fetcher_test.exs +++ b/apps/indexer/test/indexer/block_fetcher_test.exs @@ -3,6 +3,7 @@ defmodule Indexer.BlockFetcherTest do use Explorer.DataCase, async: false import ExUnit.CaptureLog + import EthereumJSONRPC.Case alias Explorer.Chain.{Address, Block, Log, Transaction, Wei} @@ -36,6 +37,10 @@ defmodule Indexer.BlockFetcherTest do # ON blocks.hash = transactions.block_hash) as blocks @first_full_block_number 37 + setup do + %{variant: EthereumJSONRPC.config(:variant)} + end + describe "start_link/1" do test "starts fetching blocks from latest and goes down" do {:ok, latest_block_number} = EthereumJSONRPC.fetch_block_number_by_tag("latest") @@ -137,104 +142,231 @@ defmodule Indexer.BlockFetcherTest do %{state: state} end - test "with single element range that is valid imports one block", %{state: state} do + test "with single element range that is valid imports one block", %{state: state, variant: variant} do {:ok, sequence} = Sequence.start_link([], 0, 1) - assert {:ok, - %{ - addresses: [ - %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> - } = address_hash - ], - blocks: [ - %Explorer.Chain.Hash{ - byte_count: 32, - bytes: - <<91, 40, 193, 191, 211, 161, 82, 48, 201, 164, 107, 57, 156, 208, 249, 166, 146, 13, 67, 46, 133, - 56, 28, 198, 161, 64, 176, 110, 132, 16, 17, 47>> - } - ], - logs: [], - transactions: [] - }} = BlockFetcher.import_range(0..0, state, sequence) + %{address_hash: address_hash, block_hash: block_hash} = + case variant do + EthereumJSONRPC.Geth -> + %{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> + }, + block_hash: %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<212, 229, 103, 64, 248, 118, 174, 248, 192, 16, 184, 106, 64, 213, 245, 103, 69, 161, 24, 208, 144, + 106, 52, 230, 154, 236, 140, 13, 177, 203, 143, 163>> + } + } + + EthereumJSONRPC.Parity -> + %{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> + }, + block_hash: %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<91, 40, 193, 191, 211, 161, 82, 48, 201, 164, 107, 57, 156, 208, 249, 166, 146, 13, 67, 46, 133, 56, + 28, 198, 161, 64, 176, 110, 132, 16, 17, 47>> + } + } + + _ -> + raise ArgumenrError, "Unsupported variant (#{variant})" + end + + log_bad_gateway( + fn -> BlockFetcher.import_range(0..0, state, sequence) end, + fn result -> + assert {:ok, + %{ + addresses: [^address_hash], + blocks: [^block_hash], + logs: [], + transactions: [] + }} = result - wait_for_tasks(InternalTransactionFetcher) - wait_for_tasks(AddressBalanceFetcher) + wait_for_tasks(InternalTransactionFetcher) + wait_for_tasks(AddressBalanceFetcher) - assert Repo.aggregate(Block, :count, :hash) == 1 - assert Repo.aggregate(Address, :count, :hash) == 1 + assert Repo.aggregate(Block, :count, :hash) == 1 + assert Repo.aggregate(Address, :count, :hash) == 1 - address = Repo.get!(Address, address_hash) + address = Repo.get!(Address, address_hash) - assert address.fetched_balance == %Wei{value: Decimal.new(0)} - assert address.fetched_balance_block_number == 0 + assert address.fetched_balance == %Wei{value: Decimal.new(0)} + assert address.fetched_balance_block_number == 0 + end + ) end - test "can import range with all synchronous imported schemas", %{state: state} do + test "can import range with all synchronous imported schemas", %{state: state, variant: variant} do {:ok, sequence} = Sequence.start_link([], 0, 1) - assert {:ok, - %{ - addresses: [ - %Explorer.Chain.Hash{ - byte_count: 20, - bytes: - <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>> - } = first_address_hash, - %Explorer.Chain.Hash{ - byte_count: 20, - bytes: - <<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122, 202>> - } = second_address_hash - ], - blocks: [ - %Explorer.Chain.Hash{ - byte_count: 32, - bytes: - <<246, 180, 184, 200, 141, 243, 235, 210, 82, 236, 71, 99, 40, 51, 77, 192, 38, 207, 102, 96, 106, - 132, 251, 118, 155, 61, 60, 188, 204, 132, 113, 189>> - } - ], - logs: [ - %{ - index: 0, - transaction_hash: %Explorer.Chain.Hash{ - byte_count: 32, - bytes: - <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, - 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> - } - } - ], - transactions: [ - %Explorer.Chain.Hash{ - byte_count: 32, - bytes: - <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, - 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> - } - ] - }} = BlockFetcher.import_range(@first_full_block_number..@first_full_block_number, state, sequence) + case variant do + EthereumJSONRPC.Geth -> + block_number = 48230 - wait_for_tasks(InternalTransactionFetcher) - wait_for_tasks(AddressBalanceFetcher) - - assert Repo.aggregate(Block, :count, :hash) == 1 - assert Repo.aggregate(Address, :count, :hash) == 2 - assert Repo.aggregate(Log, :count, :id) == 1 - assert Repo.aggregate(Transaction, :count, :hash) == 1 - - first_address = Repo.get!(Address, first_address_hash) - - assert first_address.fetched_balance == %Wei{value: Decimal.new(1)} - assert first_address.fetched_balance_block_number == @first_full_block_number - - second_address = Repo.get!(Address, second_address_hash) - - assert second_address.fetched_balance == %Wei{value: Decimal.new(252_460_837_000_000_000_000_000_000)} - assert second_address.fetched_balance_block_number == @first_full_block_number + assert {:ok, + %{ + addresses: [ + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<55, 52, 203, 24, 116, 145, 237, 231, 19, 174, 91, 59, 45, 18, 40, 74, 244, 107, 129, 1>> + } = first_address_hash, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<89, 47, 120, 202, 98, 102, 132, 20, 109, 56, 18, 133, 202, 0, 221, 145, 179, 117, 253, 17>> + } = second_address_hash, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<187, 123, 130, 135, 243, 240, 169, 51, 71, 74, 121, 234, 228, 44, 188, 169, 119, 121, 17, + 113>> + } = third_address_hash, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<210, 193, 91, 230, 52, 135, 86, 246, 145, 187, 152, 246, 13, 254, 190, 97, 230, 190, 59, + 86>> + } = fourth_address_hash, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<221, 47, 30, 110, 73, 130, 2, 232, 109, 143, 84, 66, 175, 89, 101, 128, 164, 240, 60, 44>> + } = fifth_address_hash + ], + blocks: [ + %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<209, 52, 30, 145, 228, 166, 153, 192, 47, 187, 24, 4, 84, 20, 80, 18, 144, 134, 68, 198, + 200, 119, 77, 16, 251, 182, 96, 253, 27, 146, 104, 176>> + } + ], + logs: [], + transactions: [ + %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<76, 188, 236, 37, 153, 153, 224, 115, 252, 79, 176, 224, 228, 166, 18, 66, 94, 61, 115, 57, + 47, 162, 37, 255, 36, 96, 161, 238, 171, 66, 99, 10>> + }, + %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<240, 237, 34, 44, 16, 174, 248, 135, 4, 196, 15, 198, 34, 220, 218, 174, 13, 208, 242, 122, + 154, 143, 4, 28, 171, 95, 190, 255, 254, 174, 75, 182>> + } + ] + }} = BlockFetcher.import_range(block_number..block_number, state, sequence) + + wait_for_tasks(InternalTransactionFetcher) + wait_for_tasks(AddressBalanceFetcher) + + assert Repo.aggregate(Block, :count, :hash) == 1 + assert Repo.aggregate(Address, :count, :hash) == 5 + assert Repo.aggregate(Log, :count, :id) == 0 + assert Repo.aggregate(Transaction, :count, :hash) == 2 + + first_address = Repo.get!(Address, first_address_hash) + + assert first_address.fetched_balance == %Wei{value: Decimal.new(1_999_953_415_287_753_599_000)} + assert first_address.fetched_balance_block_number == block_number + + second_address = Repo.get!(Address, second_address_hash) + + assert second_address.fetched_balance == %Wei{value: Decimal.new(50_000_000_000_000_000)} + assert second_address.fetched_balance_block_number == block_number + + third_address = Repo.get!(Address, third_address_hash) + + assert third_address.fetched_balance == %Wei{value: Decimal.new(30_827_986_037_499_360_709_544)} + assert third_address.fetched_balance_block_number == block_number + + fourth_address = Repo.get!(Address, fourth_address_hash) + + assert fourth_address.fetched_balance == %Wei{value: Decimal.new(500_000_000_001_437_727_304)} + assert fourth_address.fetched_balance_block_number == block_number + + fifth_address = Repo.get!(Address, fifth_address_hash) + + assert fifth_address.fetched_balance == %Wei{value: Decimal.new(930_417_572_224_879_702_000)} + assert fifth_address.fetched_balance_block_number == block_number + + EthereumJSONRPC.Parity -> + assert {:ok, + %{ + addresses: [ + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, + 91>> + } = first_address_hash, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122, + 202>> + } = second_address_hash + ], + blocks: [ + %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<246, 180, 184, 200, 141, 243, 235, 210, 82, 236, 71, 99, 40, 51, 77, 192, 38, 207, 102, 96, + 106, 132, 251, 118, 155, 61, 60, 188, 204, 132, 113, 189>> + } + ], + logs: [ + %{ + index: 0, + transaction_hash: %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, + 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> + } + } + ], + transactions: [ + %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, + 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> + } + ] + }} = BlockFetcher.import_range(@first_full_block_number..@first_full_block_number, state, sequence) + + wait_for_tasks(InternalTransactionFetcher) + wait_for_tasks(AddressBalanceFetcher) + + assert Repo.aggregate(Block, :count, :hash) == 1 + assert Repo.aggregate(Address, :count, :hash) == 2 + assert Repo.aggregate(Log, :count, :id) == 1 + assert Repo.aggregate(Transaction, :count, :hash) == 1 + + first_address = Repo.get!(Address, first_address_hash) + + assert first_address.fetched_balance == %Wei{value: Decimal.new(1)} + assert first_address.fetched_balance_block_number == @first_full_block_number + + second_address = Repo.get!(Address, second_address_hash) + + assert second_address.fetched_balance == %Wei{value: Decimal.new(252_460_837_000_000_000_000_000_000)} + assert second_address.fetched_balance_block_number == @first_full_block_number + + _ -> + raise ArgumentError, "Unsupport variant (#{variant})" + end end end @@ -292,7 +424,7 @@ defmodule Indexer.BlockFetcherTest do end defp wait_for_tasks(buffered_task) do - wait_until(5000, fn -> + wait_until(10_000, fn -> counts = BufferedTask.debug_count(buffered_task) counts.buffer == 0 and counts.tasks == 0 end) diff --git a/apps/indexer/test/indexer/internal_transaction_fetcher_test.exs b/apps/indexer/test/indexer/internal_transaction_fetcher_test.exs index 8960545378..2f951f788b 100644 --- a/apps/indexer/test/indexer/internal_transaction_fetcher_test.exs +++ b/apps/indexer/test/indexer/internal_transaction_fetcher_test.exs @@ -3,18 +3,18 @@ defmodule Indexer.InternalTransactionFetcherTest do import ExUnit.CaptureLog - alias Explorer.Chain.Transaction - alias Indexer.{AddressBalanceFetcherCase, InternalTransactionFetcher, PendingTransactionFetcher} + alias Indexer.{AddressBalanceFetcherCase, InternalTransactionFetcher} @moduletag :capture_log + @tag :no_geth test "does not try to fetch pending transactions from Indexer.PendingTransactionFetcher" do start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) AddressBalanceFetcherCase.start_supervised!() - start_supervised!(PendingTransactionFetcher) + start_supervised!(Indexer.PendingTransactionFetcher) wait_for_results(fn -> - Repo.one!(from(transaction in Transaction, where: is_nil(transaction.block_hash), limit: 1)) + Repo.one!(from(transaction in Explorer.Chain.Transaction, where: is_nil(transaction.block_hash), limit: 1)) end) :transaction @@ -81,6 +81,7 @@ defmodule Indexer.InternalTransactionFetcherTest do """ end + @tag :no_geth test "duplicate transaction hashes only retry uniques" do start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) AddressBalanceFetcherCase.start_supervised!() diff --git a/apps/indexer/test/indexer/pending_transaction_fetcher_test.exs b/apps/indexer/test/indexer/pending_transaction_fetcher_test.exs index 26b4496b75..e8234850b9 100644 --- a/apps/indexer/test/indexer/pending_transaction_fetcher_test.exs +++ b/apps/indexer/test/indexer/pending_transaction_fetcher_test.exs @@ -2,12 +2,13 @@ defmodule Indexer.PendingTransactionFetcherTest do # `async: false` due to use of named GenServer use Explorer.DataCase, async: false - alias Explorer.Chain.Transaction - alias Indexer.PendingTransactionFetcher - describe "start_link/1" do + @tag :no_geth # this test may fail if Sokol so low volume that the pending transactions are empty for too long test "starts fetching pending transactions" do + alias Explorer.Chain.Transaction + alias Indexer.PendingTransactionFetcher + assert Repo.aggregate(Transaction, :count, :hash) == 0 start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) diff --git a/coveralls.json b/coveralls.json index 186c305327..4341e638c9 100644 --- a/coveralls.json +++ b/coveralls.json @@ -1,7 +1,7 @@ { "coverage_options": { "treat_no_relevant_lines_as_covered": true, - "minimum_coverage": 94.4 + "minimum_coverage": 94.5 }, "terminal_options": { "file_column_width": 120