Land #335: Geth non-incompatibility

pull/392/head
Luke Imhoff 6 years ago committed by GitHub
commit fadba343e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      .circleci/config.yml
  2. 4
      apps/ethereum_jsonrpc/.formatter.exs
  3. 10
      apps/ethereum_jsonrpc/config/config.exs
  4. 5
      apps/ethereum_jsonrpc/config/geth.exs
  5. 9
      apps/ethereum_jsonrpc/config/parity.exs
  6. 104
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  7. 53
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
  8. 59
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/decode_error.ex
  9. 34
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex
  10. 89
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex
  11. 10
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex
  12. 164
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex
  13. 47
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex
  14. 47
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex
  15. 2
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex
  16. 35
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex
  17. 5
      apps/ethereum_jsonrpc/mix.exs
  18. 222
      apps/ethereum_jsonrpc/test/etheream_jsonrpc_test.exs
  19. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs
  20. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs
  21. 85
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs
  22. 10
      apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex
  23. 35
      apps/explorer/lib/explorer/chain.ex
  24. 4
      apps/explorer/lib/explorer/chain/address.ex
  25. 10
      apps/explorer/lib/explorer/chain/block.ex
  26. 2
      apps/explorer/lib/explorer/chain/data.ex
  27. 20
      apps/explorer/lib/explorer/chain/hash/address.ex
  28. 146
      apps/explorer/lib/explorer/chain/hash/nonce.ex
  29. 10
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  30. 12
      apps/explorer/lib/explorer/chain/log.ex
  31. 2
      apps/explorer/lib/explorer/chain/smart_contract.ex
  32. 54
      apps/explorer/lib/explorer/chain/transaction.ex
  33. 2
      apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs
  34. 4
      apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs
  35. 5
      apps/explorer/priv/repo/migrations/20180212222309_create_logs.exs
  36. 5
      apps/explorer/test/explorer/chain/hash/address_test.exs
  37. 5
      apps/explorer/test/explorer/chain/hash/nonce_test.exs
  38. 5
      apps/explorer/test/explorer/chain/hash/truncated_test.exs
  39. 2
      apps/explorer/test/explorer/chain/transaction_test.exs
  40. 8
      apps/explorer/test/support/factory.ex
  41. 4
      apps/indexer/.formatter.exs
  42. 2
      apps/indexer/lib/indexer/address_balance_fetcher.ex
  43. 11
      apps/indexer/lib/indexer/block_fetcher.ex
  44. 3
      apps/indexer/lib/indexer/internal_transaction_fetcher.ex
  45. 30
      apps/indexer/lib/indexer/pending_transaction_fetcher.ex
  46. 154
      apps/indexer/test/indexer/address_balance_fetcher_test.exs
  47. 308
      apps/indexer/test/indexer/block_fetcher_test.exs
  48. 9
      apps/indexer/test/indexer/internal_transaction_fetcher_test.exs
  49. 7
      apps/indexer/test/indexer/pending_transaction_fetcher_test.exs
  50. 2
      coveralls.json

@ -296,7 +296,7 @@ jobs:
name: Scan explorer_web for vulnerabilities name: Scan explorer_web for vulnerabilities
command: mix sobelow --config command: mix sobelow --config
working_directory: "apps/explorer_web" working_directory: "apps/explorer_web"
test: test_geth:
docker: docker:
# Ensure .tool-versions matches # Ensure .tool-versions matches
- image: circleci/elixir:1.6.5-node-browsers - image: circleci/elixir:1.6.5-node-browsers
@ -306,6 +306,7 @@ jobs:
PGPASSWORD: postgres PGPASSWORD: postgres
# match POSTGRES_USER for postgres image below # match POSTGRES_USER for postgres image below
PGUSER: postgres PGUSER: postgres
ETHEREUM_JSONRPC_VARIANT: geth
- image: circleci/postgres:10.3-alpine - image: circleci/postgres:10.3-alpine
environment: environment:
# Match apps/explorer/config/test.exs config :explorerer, Explorer.Repo, database # Match apps/explorer/config/test.exs config :explorerer, Explorer.Repo, database
@ -328,7 +329,44 @@ jobs:
name: Wait for DB name: Wait for DB
command: dockerize -wait tcp://localhost:5432 -timeout 1m 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: - store_test_results:
path: _build/test/junit path: _build/test/junit
@ -356,7 +394,8 @@ workflows:
- eslint - eslint
- jest - jest
- sobelow - sobelow
- test - test_parity
- test_geth
- dialyzer: - dialyzer:
requires: requires:
- build - build
@ -372,6 +411,9 @@ workflows:
- sobelow: - sobelow:
requires: requires:
- build - build
- test: - test_parity:
requires:
- build
- test_geth:
requires: requires:
- build - build

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

@ -3,6 +3,10 @@
use Mix.Config use Mix.Config
config :ethereum_jsonrpc, config :ethereum_jsonrpc,
http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :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" 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"

@ -0,0 +1,5 @@
use Mix.Config
config :ethereum_jsonrpc,
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY",
variant: EthereumJSONRPC.Geth

@ -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

@ -19,7 +19,7 @@ defmodule EthereumJSONRPC do
require Logger require Logger
alias Explorer.Chain.Block alias Explorer.Chain.Block
alias EthereumJSONRPC.{Blocks, Parity, Receipts, Transactions} alias EthereumJSONRPC.{Blocks, Receipts, Transactions}
@typedoc """ @typedoc """
Truncated 20-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a 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) Application.fetch_env!(:ethereum_jsonrpc, key)
end 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 """ @doc """
Fetches balance for each address `hash` at the `block_number` Fetches balance for each address `hash` at the `block_number`
""" """
@ -108,7 +129,7 @@ defmodule EthereumJSONRPC do
with {:ok, responses} <- with {:ok, responses} <-
id_to_params id_to_params
|> get_balance_requests() |> 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) get_balance_responses_to_addresses_params(responses, id_to_params)
end end
end end
@ -121,7 +142,7 @@ defmodule EthereumJSONRPC do
def fetch_blocks_by_hash(block_hashes) do def fetch_blocks_by_hash(block_hashes) do
block_hashes block_hashes
|> get_block_by_hash_requests() |> get_block_by_hash_requests()
|> json_rpc(config(:url)) |> json_rpc(method_to_url(:eth_getBlockByHash))
|> handle_get_blocks() |> handle_get_blocks()
|> case do |> case do
{:ok, _next, results} -> {:ok, results} {:ok, _next, results} -> {:ok, results}
@ -135,18 +156,13 @@ defmodule EthereumJSONRPC do
def fetch_blocks_by_range(_first.._last = range) do def fetch_blocks_by_range(_first.._last = range) do
range range
|> get_block_by_number_requests() |> get_block_by_number_requests()
|> json_rpc(config(:url)) |> json_rpc(method_to_url(:eth_getBlockByNumber))
|> handle_get_blocks() |> handle_get_blocks()
end end
@doc """ @doc """
Fetches block number by `t:tag/0`. 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 ## Returns
* `{:ok, number}` - the block number for the given `tag`. * `{: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 def fetch_block_number_by_tag(tag) when tag in ~w(earliest latest pending) do
tag tag
|> get_block_by_tag_request() |> get_block_by_tag_request()
|> json_rpc(config(:url)) |> json_rpc(method_to_url(:eth_getBlockByNumber))
|> handle_get_block_by_tag() |> handle_get_block_by_tag()
end end
@doc """ @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 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 end
def fetch_transaction_receipts(hashes) when is_list(hashes) do @doc """
Receipts.fetch(hashes) 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 end
@doc """ @doc """
@ -200,8 +226,14 @@ defmodule EthereumJSONRPC do
json = encode_json(payload) json = encode_json(payload)
case post(url, json, config(:http)) do case post(url, json, config(:http)) do
{:ok, %HTTPoison.Response{body: body, status_code: code}} -> {:ok, %HTTPoison.Response{body: body, status_code: status_code}} ->
body |> decode_json(code, json, url) |> handle_response(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, %HTTPoison.Error{reason: reason}} ->
{:error, reason} {:error, reason}
@ -272,8 +304,10 @@ defmodule EthereumJSONRPC do
rechunk_json_rpc(url, chunks, options, response, decoded_response_bodies) rechunk_json_rpc(url, chunks, options, response, decoded_response_bodies)
{:ok, %HTTPoison.Response{body: body, status_code: status_code}} -> {:ok, %HTTPoison.Response{body: body, status_code: status_code}} ->
decoded_body = decode_json(body, status_code, json, url) with {:ok, decoded_body} <-
chunked_json_rpc(url, tail, options, [decoded_body | decoded_response_bodies]) 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, %HTTPoison.Error{reason: reason}} ->
{:error, reason} {:error, reason}
@ -380,7 +414,7 @@ defmodule EthereumJSONRPC do
defp get_block_by_tag_request(tag) do defp get_block_by_tag_request(tag) do
# eth_getBlockByNumber accepts either a number OR a tag # 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 end
defp get_block_by_number_params(options) do 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 encode_json(data), do: Jason.encode_to_iodata!(data)
defp decode_json(response_body, response_status_code, request_body, request_url) do defp decode_json(named_arguments) when is_list(named_arguments) do
Jason.decode!(response_body) response = Keyword.fetch!(named_arguments, :response)
rescue response_body = Keyword.fetch!(response, :body)
Jason.DecodeError ->
Logger.error(fn ->
"""
failed to decode json payload:
request url: #{inspect(request_url)}
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)} _ ->
""" raise EthereumJSONRPC.DecodeError, named_arguments
end) end
end
raise("bad jason")
end end
defp handle_get_blocks({:ok, results}) do defp handle_get_blocks({:ok, results}) do

@ -4,7 +4,7 @@ defmodule EthereumJSONRPC.Block do
and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber). 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
alias EthereumJSONRPC.Transactions alias EthereumJSONRPC.Transactions
@ -22,6 +22,8 @@ defmodule EthereumJSONRPC.Block do
for the logs of the block. `nil` when block is pending. 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 * `"miner"` - `t:EthereumJSONRPC.address/0` of the beneficiary to whom the mining rewards were given. Aliased by
`"author"`. `"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. * `"nonce"` - `t:EthereumJSONRPC.nonce/0`. `nil` when its pending block.
* `"number"` - the block number `t:EthereumJSONRPC.quantity/0`. `nil` when block is pending. * `"number"` - the block number `t:EthereumJSONRPC.quantity/0`. `nil` when block is pending.
* `"parentHash" - the `t:EthereumJSONRPC.hash/0` of the parent block. * `"parentHash" - the `t:EthereumJSONRPC.hash/0` of the parent block.
@ -92,11 +94,50 @@ defmodule EthereumJSONRPC.Block do
total_difficulty: 340282366920938463463374607431465668165 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 @spec elixir_to_params(elixir) :: map
def elixir_to_params( def elixir_to_params(
%{ %{
"author" => miner_hash,
"difficulty" => difficulty, "difficulty" => difficulty,
"gasLimit" => gas_limit, "gasLimit" => gas_limit,
"gasUsed" => gas_used, "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 # `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct
# hash format # hash format
defp entry_to_elixir({key, _} = entry) defp entry_to_elixir({key, _} = entry)
when key in ~w(author extraData hash logsBloom miner parentHash receiptsRoot sealFields sha3Uncles signature when key in ~w(author extraData hash logsBloom miner mixHash nonce parentHash receiptsRoot sealFields sha3Uncles
stateRoot step transactionsRoot uncles), signature stateRoot step transactionsRoot uncles),
do: entry do: entry
defp entry_to_elixir({"nonce" = key, nonce}) do
{key, nonce_to_integer(nonce)}
end
defp entry_to_elixir({"timestamp" = key, timestamp}) do defp entry_to_elixir({"timestamp" = key, timestamp}) do
{key, timestamp_to_datetime(timestamp)} {key, timestamp_to_datetime(timestamp)}
end end

@ -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

@ -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

@ -50,25 +50,55 @@ defmodule EthereumJSONRPC.Log do
type: "mined" 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(%{ def elixir_to_params(
"address" => address_hash, %{
"blockNumber" => block_number, "address" => address_hash,
"data" => data, "blockNumber" => block_number,
"logIndex" => index, "data" => data,
"topics" => topics, "logIndex" => index,
"transactionHash" => transaction_hash, "topics" => topics,
"type" => type "transactionHash" => transaction_hash
}) do } = elixir
) do
%{ %{
address_hash: address_hash, address_hash: address_hash,
block_number: block_number, block_number: block_number,
data: data, data: data,
index: index, index: index,
transaction_hash: transaction_hash, transaction_hash: transaction_hash
type: type
} }
|> put_topics(topics) |> put_topics(topics)
|> put_type(elixir)
end end
@doc """ @doc """
@ -101,6 +131,37 @@ defmodule EthereumJSONRPC.Log do
"type" => "mined" "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 def to_elixir(log) when is_map(log) do
Enum.into(log, %{}, &entry_to_elixir/1) 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(:third_topic, Enum.at(topics, 2))
|> Map.put(:fourth_topic, Enum.at(topics, 3)) |> Map.put(:fourth_topic, Enum.at(topics, 3))
end end
defp put_type(params, %{"type" => type}) do
Map.put(params, :type, type)
end
defp put_type(params, _), do: params
end end

@ -3,21 +3,24 @@ defmodule EthereumJSONRPC.Parity do
Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/). 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.Parity.Traces
alias EthereumJSONRPC.{Transaction, Transactions} alias EthereumJSONRPC.{Transaction, Transactions}
@behaviour EthereumJSONRPC.Variant
@doc """ @doc """
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Parity trace URL. 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 def fetch_internal_transactions(transactions_params) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params) id_to_params = id_to_params(transactions_params)
with {:ok, responses} <- with {:ok, responses} <-
id_to_params id_to_params
|> trace_replay_transaction_requests() |> 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) trace_replay_transaction_responses_to_internal_transactions_params(responses, id_to_params)
end end
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 *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. 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} @spec fetch_pending_transactions() :: {:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions do def fetch_pending_transactions do
with {:ok, transactions} <- with {:ok, transactions} <-
%{id: 1, method: "parity_pendingTransactions", params: []} %{id: 1, method: "parity_pendingTransactions", params: []}
|> request() |> request()
|> json_rpc(config(:url)) do |> json_rpc(method_to_url(:parity_pendingTransactions)) do
transactions_params = transactions_params =
transactions transactions
|> Transactions.to_elixir() |> Transactions.to_elixir()

@ -19,12 +19,14 @@ defmodule EthereumJSONRPC.Receipt do
* `"blockNumber"` - The block number `t:EthereumJSONRPC.quanity/0`. * `"blockNumber"` - The block number `t:EthereumJSONRPC.quanity/0`.
* `"cumulativeGasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used when this transaction was executed in the * `"cumulativeGasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used when this transaction was executed in the
block. 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. * `"gasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used by this specific transaction alone.
* `"logs"` - `t:list/0` of log objects, which this transaction generated. * `"logs"` - `t:list/0` of log objects, which this transaction generated.
* `"logsBloom"` - `t:EthereumJSONRPC.data/0` of 256 Bytes for * `"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. [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) * `"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) * `"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. * `"transactionHash"` - `t:EthereumJSONRPC.hash/0` the transaction.
* `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the transaction index in the block. * `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the transaction index in the block.
""" """
@ -70,6 +72,87 @@ defmodule EthereumJSONRPC.Receipt do
transaction_index: 0 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) :: %{ @spec elixir_to_params(elixir) :: %{
cumulative_gas_used: non_neg_integer, cumulative_gas_used: non_neg_integer,
@ -78,13 +161,16 @@ defmodule EthereumJSONRPC.Receipt do
transaction_hash: String.t(), transaction_hash: String.t(),
transaction_index: non_neg_integer() transaction_index: non_neg_integer()
} }
def elixir_to_params(%{ def elixir_to_params(
"cumulativeGasUsed" => cumulative_gas_used, %{
"gasUsed" => gas_used, "cumulativeGasUsed" => cumulative_gas_used,
"status" => status, "gasUsed" => gas_used,
"transactionHash" => transaction_hash, "transactionHash" => transaction_hash,
"transactionIndex" => transaction_index "transactionIndex" => transaction_index
}) do } = elixir
) do
status = elixir_to_status(elixir)
%{ %{
cumulative_gas_used: cumulative_gas_used, cumulative_gas_used: cumulative_gas_used,
gas_used: gas_used, gas_used: gas_used,
@ -126,19 +212,77 @@ defmodule EthereumJSONRPC.Receipt do
"transactionIndex" => 0 "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 @spec to_elixir(t) :: elixir
def to_elixir(receipt) when is_map(receipt) do def to_elixir(receipt) when is_map(receipt) do
Enum.into(receipt, %{}, &entry_to_elixir/1) Enum.into(receipt, %{}, &entry_to_elixir/1)
end 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 # 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 # `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct
# hash format # hash format
defp entry_to_elixir({key, _} = entry) when key in ~w(blockHash contractAddress logsBloom root transactionHash), # gas is passsed in from the `t:EthereumJSONRPC.Transaction.params/0` to allow pre-Byzantium status to be derived
do: entry 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)} {key, quantity_to_integer(quantity)}
end end

@ -111,15 +111,31 @@ defmodule EthereumJSONRPC.Receipts do
Enum.map(elixir, &Receipt.elixir_to_params/1) Enum.map(elixir, &Receipt.elixir_to_params/1)
end end
def fetch(hashes) when is_list(hashes) do @spec fetch([
hashes %{
|> Enum.map(&hash_to_json/1) 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)) |> json_rpc(config(:url))
|> case do |> case do
{:ok, responses} -> {:ok, responses} ->
elixir_receipts = elixir_receipts =
responses responses
|> responses_to_receipts() |> responses_to_receipts(id_to_transaction_params)
|> to_elixir() |> to_elixir()
elixir_logs = elixir_to_logs(elixir_receipts) elixir_logs = elixir_to_logs(elixir_receipts)
@ -199,18 +215,29 @@ defmodule EthereumJSONRPC.Receipts do
Enum.map(receipts, &Receipt.to_elixir/1) Enum.map(receipts, &Receipt.to_elixir/1)
end 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", "jsonrpc" => "2.0",
"method" => "eth_getTransactionReceipt", "method" => "eth_getTransactionReceipt",
"params" => [hash] "params" => [transaction_hash]
} }
end 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 defp response_to_receipt(%{"id" => id, "result" => receipt}, id_to_transaction_params) do
Enum.map(responses, &response_to_receipt/1) 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
end end

@ -56,15 +56,52 @@ defmodule EthereumJSONRPC.Transaction do
index: non_neg_integer(), index: non_neg_integer(),
input: String.t(), input: String.t(),
nonce: non_neg_integer(), nonce: non_neg_integer(),
public_key: String.t(),
r: non_neg_integer(), r: non_neg_integer(),
s: non_neg_integer(), s: non_neg_integer(),
standard_v: 0 | 1,
to_address_hash: EthereumJSONRPC.address(), to_address_hash: EthereumJSONRPC.address(),
v: non_neg_integer(), v: non_neg_integer(),
value: 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 @spec elixir_to_params(elixir) :: params
def elixir_to_params(%{ def elixir_to_params(%{
"blockHash" => block_hash, "blockHash" => block_hash,
@ -75,10 +112,8 @@ defmodule EthereumJSONRPC.Transaction do
"hash" => hash, "hash" => hash,
"input" => input, "input" => input,
"nonce" => nonce, "nonce" => nonce,
"publicKey" => public_key,
"r" => r, "r" => r,
"s" => s, "s" => s,
"standardV" => standard_v,
"to" => to_address_hash, "to" => to_address_hash,
"transactionIndex" => index, "transactionIndex" => index,
"v" => v, "v" => v,
@ -94,10 +129,8 @@ defmodule EthereumJSONRPC.Transaction do
index: index, index: index,
input: input, input: input,
nonce: nonce, nonce: nonce,
public_key: public_key,
r: r, r: r,
s: s, s: s,
standard_v: standard_v,
to_address_hash: to_address_hash, to_address_hash: to_address_hash,
v: v, v: v,
value: value value: value
@ -117,10 +150,8 @@ defmodule EthereumJSONRPC.Transaction do
...> index: 0, ...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0, ...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75", ...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3", ...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: 0,
...> v: "0x8d", ...> v: "0x8d",
...> value: 0 ...> value: 0
...> } ...> }

@ -50,10 +50,8 @@ defmodule EthereumJSONRPC.Transactions do
index: 0, index: 0,
input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
nonce: 0, nonce: 0,
public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
r: "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75", r: "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3", s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
standard_v: "0x0",
to_address_hash: nil, to_address_hash: nil,
v: "0xbd", v: "0xbd",
value: 0 value: 0

@ -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

@ -15,6 +15,7 @@ defmodule EthereumJsonrpc.MixProject do
ignore_warnings: "../../.dialyzer-ignore" ignore_warnings: "../../.dialyzer-ignore"
], ],
elixir: "~> 1.6", elixir: "~> 1.6",
elixirc_paths: elixirc_paths(Mix.env()),
lockfile: "../../mix.lock", lockfile: "../../mix.lock",
preferred_cli_env: [ preferred_cli_env: [
coveralls: :test, coveralls: :test,
@ -45,6 +46,10 @@ defmodule EthereumJsonrpc.MixProject do
] ++ env_aliases(env) ] ++ env_aliases(env)
end 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(:dev), do: []
defp env_aliases(_env), do: [compile: "compile --warnings-as-errors"] defp env_aliases(_env), do: [compile: "compile --warnings-as-errors"]

@ -1,130 +1,134 @@
defmodule EthereumJSONRPCTest do defmodule EthereumJSONRPCTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
doctest EthereumJSONRPC import EthereumJSONRPC.Case
describe "fetch_balances/1" do @moduletag :capture_log
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
test "with a mix of valid and invalid hash_data returns {:error, reasons}" do setup do
assert EthereumJSONRPC.fetch_balances([ %{variant: EthereumJSONRPC.config(:variant)}
# 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
end end
describe "json_rpc/2" do describe "fetch_balances/1" do
# regression test for https://github.com/poanetwork/poa-explorer/issues/254 test "with all valid hash_data returns {:ok, addresses_params}", %{variant: variant} do
test "transparently splits batch payloads that would trigger a 413 Request Entity Too Large" do assert {:ok,
block_numbers = 0..13000 [
%{
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 = test "with all invalid hash_data returns {:error, reasons}", %{variant: variant} do
block_numbers assert {:error, reasons} = EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}])
|> Stream.with_index() assert is_list(reasons)
|> Enum.map(&get_block_by_number_request/1) 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) case variant do
assert Enum.count(responses) == Enum.count(block_numbers) 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}} -> raise ArgumentError, "Unsupported variant (#{variant}})"
EthereumJSONRPC.quantity_to_integer(quantity) end
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
end end
defp assert_payload_too_large(payload) do describe "fetch_block_number_by_tag/1" do
json = Jason.encode_to_iodata!(payload) @tag capture_log: false
headers = [{"Content-Type", "application/json"}] test "with earliest" do
url = EthereumJSONRPC.config(:url) log_bad_gateway(
fn -> EthereumJSONRPC.fetch_block_number_by_tag("earliest") end,
assert {:ok, %HTTPoison.Response{body: body, status_code: 413}} = fn result ->
HTTPoison.post(url, json, headers, EthereumJSONRPC.config(:http)) assert {:ok, 0} = result
end
)
end
assert body =~ "413 Request Entity Too Large" @tag capture_log: false
end 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 @tag capture_log: false
%{ test "with pending" do
"id" => id, log_bad_gateway(
"jsonrpc" => "2.0", fn -> EthereumJSONRPC.fetch_block_number_by_tag("pending") end,
"method" => "eth_getBlockByNumber", fn result ->
"params" => [EthereumJSONRPC.integer_to_quantity(block_number), true] assert {:ok, number} = result
} assert number > 0
end
)
end
end end
end end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.GethTest do
use ExUnit.Case, async: false
doctest EthereumJSONRPC.Geth
end

@ -1,9 +1,8 @@
defmodule EthereumJSONRPC.ParityTest do defmodule EthereumJSONRPC.ParityTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
doctest EthereumJSONRPC.Parity
describe "fetch_internal_transactions/1" do describe "fetch_internal_transactions/1" do
@tag :no_geth
test "with all valid transaction_params returns {:ok, transactions_params}" do test "with all valid transaction_params returns {:ok, transactions_params}" do
assert EthereumJSONRPC.Parity.fetch_internal_transactions([ assert EthereumJSONRPC.Parity.fetch_internal_transactions([
%{ %{
@ -33,6 +32,7 @@ defmodule EthereumJSONRPC.ParityTest do
} }
end end
@tag :no_geth
test "with all invalid transaction_params returns {:error, reasons}" do test "with all invalid transaction_params returns {:error, reasons}" do
assert EthereumJSONRPC.Parity.fetch_internal_transactions([ assert EthereumJSONRPC.Parity.fetch_internal_transactions([
%{ %{
@ -53,6 +53,7 @@ defmodule EthereumJSONRPC.ParityTest do
]} ]}
end end
@tag :no_geth
test "with a mix of valid and invalid transaction_params returns {:error, reasons}" do test "with a mix of valid and invalid transaction_params returns {:error, reasons}" do
assert EthereumJSONRPC.Parity.fetch_internal_transactions([ assert EthereumJSONRPC.Parity.fetch_internal_transactions([
# start with :ok # start with :ok

@ -3,6 +3,10 @@ defmodule EthereumJSONRPC.ReceiptsTest do
alias EthereumJSONRPC.Receipts alias EthereumJSONRPC.Receipts
setup do
%{variant: EthereumJSONRPC.config(:variant)}
end
doctest Receipts doctest Receipts
# These are integration tests that depend on the sokol chain being used. sokol can be used with the following config # 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" # url: "https://sokol.poa.network"
# #
describe "fetch/1" do describe "fetch/1" do
test "with receipts and logs" do test "with receipts and logs", %{variant: variant} do
assert {:ok, case variant do
%{ EthereumJSONRPC.Geth ->
logs: [ assert {:ok,
%{ %{
address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", logs: [],
data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", receipts: [
first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22", %{
fourth_topic: nil, cumulative_gas_used: 1_238_877,
index: 0, gas_used: 21000,
second_topic: nil, status: :ok,
third_topic: nil, transaction_hash: "0x360fb62cc817093e5624468735803ea39cad719e5c68ca322bae6ba4f520756f",
transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", transaction_index: 57
type: "mined" }
} ]
], }} =
receipts: [ Receipts.fetch([
%{
gas: 90000,
hash: "0x360fb62cc817093e5624468735803ea39cad719e5c68ca322bae6ba4f520756f"
}
])
EthereumJSONRPC.Parity ->
assert {:ok,
%{ %{
cumulative_gas_used: 50450, logs: [
gas_used: 50450, %{
status: :ok, address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
transaction_index: 0 first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22",
} fourth_topic: nil,
] index: 0,
}} = Receipts.fetch(["0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5"]) 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 end
end end

@ -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

@ -178,7 +178,7 @@ defmodule Explorer.Chain do
""" """
@spec address_to_transactions(Address.t(), [paging_options | necessity_by_association_option]) :: [Transaction.t()] @spec address_to_transactions(Address.t(), [paging_options | necessity_by_association_option]) :: [Transaction.t()]
def address_to_transactions( 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 \\ [] options \\ []
) )
when is_list(options) do when is_list(options) do
@ -356,7 +356,7 @@ defmodule Explorer.Chain do
[ [
[{:addresses, [timeout_option]}] | timeout_option [{: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 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 with {:ok, changes_list} <- changes_list(addresses_params, for: Address, with: :balance_changeset) do
timestamps = timestamps() timestamps = timestamps()
@ -504,12 +504,12 @@ defmodule Explorer.Chain do
...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0"} ...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0"}
...> ) ...> )
...> errors ...> 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( iex> {:error, %Ecto.Changeset{errors: errors}} = Explorer.Chain.create_address(
...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0ba"} ...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0ba"}
...> ) ...> )
...> errors ...> 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()} @spec create_address(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
@ -618,8 +618,8 @@ defmodule Explorer.Chain do
{:error, :not_found} {:error, :not_found}
""" """
@spec hash_to_address(Hash.Truncated.t()) :: {:ok, Address.t()} | {:error, :not_found} @spec hash_to_address(Hash.Address.t()) :: {:ok, Address.t()} | {:error, :not_found}
def hash_to_address(%Hash{byte_count: unquote(Hash.Truncated.byte_count())} = hash) do def hash_to_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do
query = query =
from( from(
address in Address, address in Address,
@ -635,7 +635,7 @@ defmodule Explorer.Chain do
end end
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 = query =
from( from(
address in Address, address in Address,
@ -804,10 +804,8 @@ defmodule Explorer.Chain do
...> index: 0, ...> index: 0,
...> input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", ...> input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> nonce: 4, ...> nonce: 4,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01, ...> r: 0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01,
...> s: 0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f, ...> s: 0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f,
...> standard_v: 1,
...> status: :ok, ...> status: :ok,
...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", ...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> v: 0xbe, ...> v: 0xbe,
@ -965,10 +963,8 @@ defmodule Explorer.Chain do
...> index: 0, ...> index: 0,
...> input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", ...> input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> nonce: 4, ...> nonce: 4,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01, ...> r: 0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01,
...> s: 0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f, ...> s: 0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f,
...> standard_v: 1,
...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", ...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> v: 0xbe, ...> v: 0xbe,
...> value: 0 ...> value: 0
@ -1182,7 +1178,7 @@ defmodule Explorer.Chain do
]) :: ]) ::
{:ok, {:ok,
%{ %{
optional(:addresses) => [Hash.Truncated.t()], optional(:addresses) => [Hash.Address.t()],
optional(:blocks) => [Hash.Full.t()], optional(:blocks) => [Hash.Full.t()],
optional(:internal_transactions) => [ optional(:internal_transactions) => [
%{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()} %{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()}
@ -1233,7 +1229,7 @@ defmodule Explorer.Chain do
]) :: ]) ::
{:ok, {:ok,
%{ %{
optional(:addresses) => [Hash.Truncated.t()], optional(:addresses) => [Hash.Address.t()],
optional(:internal_transactions) => [ optional(:internal_transactions) => [
%{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()} %{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()}
] ]
@ -1634,7 +1630,7 @@ defmodule Explorer.Chain do
@spec stream_unfetched_addresses( @spec stream_unfetched_addresses(
initial :: accumulator, initial :: accumulator,
reducer :: 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} ) :: {:ok, accumulator}
when accumulator: term() when accumulator: term()
def stream_unfetched_addresses(initial, reducer) when is_function(reducer, 2) do def stream_unfetched_addresses(initial, reducer) when is_function(reducer, 2) do
@ -1713,10 +1709,8 @@ defmodule Explorer.Chain do
| :index | :index
| :input | :input
| :nonce | :nonce
| :public_key
| :r | :r
| :s | :s
| :standard_v
| :to_address_hash | :to_address_hash
| :v | :v
| :value | :value
@ -1991,7 +1985,7 @@ defmodule Explorer.Chain do
end end
@doc """ @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") iex> Explorer.Chain.string_to_address_hash("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
{ {
@ -2008,9 +2002,9 @@ defmodule Explorer.Chain do
:error :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 def string_to_address_hash(string) when is_binary(string) do
Hash.Truncated.cast(string) Hash.Address.cast(string)
end end
@doc """ @doc """
@ -2274,8 +2268,7 @@ defmodule Explorer.Chain do
) )
end end
@spec insert_addresses([%{hash: Hash.Truncated.t()}], [timeout_option | timestamps_option]) :: @spec insert_addresses([%{hash: Hash.Address.t()}], [timeout_option | timestamps_option]) :: {:ok, [Hash.Address.t()]}
{:ok, [Hash.Truncated.t()]}
defp insert_addresses(changes_list, named_arguments) defp insert_addresses(changes_list, named_arguments)
when is_list(changes_list) and is_list(named_arguments) do when is_list(changes_list) and is_list(named_arguments) do
timestamps = Keyword.fetch!(named_arguments, :timestamps) timestamps = Keyword.fetch!(named_arguments, :timestamps)

@ -29,13 +29,13 @@ defmodule Explorer.Chain.Address do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
fetched_balance: Wei.t(), fetched_balance: Wei.t(),
fetched_balance_block_number: Block.block_number(), fetched_balance_block_number: Block.block_number(),
hash: Hash.Truncated.t(), hash: Hash.Address.t(),
contract_code: Data.t() | nil, contract_code: Data.t() | nil,
inserted_at: DateTime.t(), inserted_at: DateTime.t(),
updated_at: DateTime.t() updated_at: DateTime.t()
} }
@primary_key {:hash, Hash.Truncated, autogenerate: false} @primary_key {:hash, Hash.Address, autogenerate: false}
schema "addresses" do schema "addresses" do
field(:fetched_balance, Wei) field(:fetched_balance, Wei)
field(:fetched_balance_block_number, :integer) field(:fetched_balance_block_number, :integer)

@ -46,10 +46,10 @@ defmodule Explorer.Chain.Block do
difficulty: difficulty(), difficulty: difficulty(),
gas_limit: Gas.t(), gas_limit: Gas.t(),
gas_used: Gas.t(), gas_used: Gas.t(),
hash: Hash.t(), hash: Hash.Full.t(),
miner: %Ecto.Association.NotLoaded{} | Address.t(), miner: %Ecto.Association.NotLoaded{} | Address.t(),
miner_hash: Hash.Truncated.t(), miner_hash: Hash.Address.t(),
nonce: Hash.t(), nonce: Hash.Nonce.t(),
number: block_number(), number: block_number(),
parent_hash: Hash.t(), parent_hash: Hash.t(),
size: non_neg_integer(), size: non_neg_integer(),
@ -63,7 +63,7 @@ defmodule Explorer.Chain.Block do
field(:difficulty, :decimal) field(:difficulty, :decimal)
field(:gas_limit, :integer) field(:gas_limit, :integer)
field(:gas_used, :integer) field(:gas_used, :integer)
field(:nonce, :integer) field(:nonce, Hash.Nonce)
field(:number, :integer) field(:number, :integer)
field(:size, :integer) field(:size, :integer)
field(:timestamp, :utc_datetime) field(:timestamp, :utc_datetime)
@ -71,7 +71,7 @@ defmodule Explorer.Chain.Block do
timestamps() 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) belongs_to(:parent, __MODULE__, foreign_key: :parent_hash, references: :hash, type: Hash.Full)
has_many(:transactions, Transaction) has_many(:transactions, Transaction)
end end

@ -81,7 +81,7 @@ defmodule Explorer.Chain.Data do
{:ok, %Explorer.Chain.Data{bytes: <<>>}} {:ok, %Explorer.Chain.Data{bytes: <<>>}}
Hashes can be represented as `Explorer.Chain.Data`, but it is better to use `Explorer.Chain.Hash.Full` or 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") iex> Explorer.Chain.Data.cast("0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")
{ {

@ -1,4 +1,4 @@
defmodule Explorer.Chain.Hash.Truncated do defmodule Explorer.Chain.Hash.Address do
@moduledoc """ @moduledoc """
The address (40 (hex) characters / 160 bits / 20 bytes) is derived from the public key (128 (hex) characters / 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). 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 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{ ...> %Explorer.Chain.Hash{
...> byte_count: 20, ...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>> ...> 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` 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, :ok,
%Explorer.Chain.Hash{ %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. 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 :error
If the `term` is a `String.t` that starts with `0x`, then is converted to an integer and then to `t:t/0`. 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, :ok,
%Explorer.Chain.Hash{ %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), 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. `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 :error
""" """
@ -83,7 +83,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the field from the struct is `t:t/0`, then it succeeds 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{ ...> %Explorer.Chain.Hash{
...> byte_count: 20, ...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>> ...> 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 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{ ...> %Explorer.Chain.Hash{
...> byte_count: 32, ...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
@ -114,7 +114,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the binary hash is the correct format, it is returned. 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)>> ...> <<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. 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)>> ...> <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
...> ) ...> )
:error :error

@ -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

@ -33,7 +33,7 @@ defmodule Explorer.Chain.InternalTransaction do
created_contract_code: Data.t() | nil, created_contract_code: Data.t() | nil,
error: String.t(), error: String.t(),
from_address: %Ecto.Association.NotLoaded{} | Address.t(), from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_hash: Hash.Truncated.t(), from_address_hash: Hash.Address.t(),
gas: Gas.t(), gas: Gas.t(),
gas_used: Gas.t() | nil, gas_used: Gas.t() | nil,
index: non_neg_integer(), index: non_neg_integer(),
@ -41,7 +41,7 @@ defmodule Explorer.Chain.InternalTransaction do
input: Data.t(), input: Data.t(),
output: Data.t() | nil, output: Data.t() | nil,
to_address: %Ecto.Association.NotLoaded{} | Address.t(), to_address: %Ecto.Association.NotLoaded{} | Address.t(),
to_address_hash: Hash.Truncated.t(), to_address_hash: Hash.Address.t(),
trace_address: [non_neg_integer()], trace_address: [non_neg_integer()],
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Explorer.Chain.Hash.t(), transaction_hash: Explorer.Chain.Hash.t(),
@ -70,7 +70,7 @@ defmodule Explorer.Chain.InternalTransaction do
Address, Address,
foreign_key: :created_contract_address_hash, foreign_key: :created_contract_address_hash,
references: :hash, references: :hash,
type: Hash.Truncated type: Hash.Address
) )
belongs_to( belongs_to(
@ -78,7 +78,7 @@ defmodule Explorer.Chain.InternalTransaction do
Address, Address,
foreign_key: :from_address_hash, foreign_key: :from_address_hash,
references: :hash, references: :hash,
type: Hash.Truncated type: Hash.Address
) )
belongs_to( belongs_to(
@ -86,7 +86,7 @@ defmodule Explorer.Chain.InternalTransaction do
Address, Address,
foreign_key: :to_address_hash, foreign_key: :to_address_hash,
references: :hash, references: :hash,
type: Hash.Truncated type: Hash.Address
) )
belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)

@ -5,8 +5,8 @@ defmodule Explorer.Chain.Log do
alias Explorer.Chain.{Address, Data, Hash, Transaction} alias Explorer.Chain.{Address, Data, Hash, Transaction}
@required_attrs ~w(address_hash data index transaction_hash type)a @required_attrs ~w(address_hash data index transaction_hash)a
@optional_attrs ~w(first_topic second_topic third_topic fourth_topic)a @optional_attrs ~w(first_topic second_topic third_topic fourth_topic type)a
@typedoc """ @typedoc """
* `address` - address of contract that generate the event * `address` - address of contract that generate the event
@ -19,11 +19,11 @@ defmodule Explorer.Chain.Log do
* `transaction` - transaction for which `log` is * `transaction` - transaction for which `log` is
* `transaction_hash` - foreign key for `transaction`. * `transaction_hash` - foreign key for `transaction`.
* `third_topic` - `topics[2]` * `third_topic` - `topics[2]`
* `type` - type of event * `type` - type of event. *Parity-only*
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(), address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Truncated.t(), address_hash: Hash.Address.t(),
data: Data.t(), data: Data.t(),
first_topic: String.t(), first_topic: String.t(),
fourth_topic: String.t(), fourth_topic: String.t(),
@ -32,7 +32,7 @@ defmodule Explorer.Chain.Log do
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Hash.Full.t(), transaction_hash: Hash.Full.t(),
third_topic: String.t(), third_topic: String.t(),
type: String.t() type: String.t() | nil
} }
schema "logs" do schema "logs" do
@ -46,7 +46,7 @@ defmodule Explorer.Chain.Log do
timestamps() 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) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)
end end

@ -32,7 +32,7 @@ defmodule Explorer.Chain.SmartContract do
Address, Address,
foreign_key: :address_hash, foreign_key: :address_hash,
references: :hash, references: :hash,
type: Hash.Truncated type: Hash.Address
) )
timestamps() timestamps()

@ -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 @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 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 @required_attrs ~w(gas gas_price hash input nonce r s v value)a
@typedoc """
The full public key of the signer of the transaction.
"""
@type public_key :: Data.t()
@typedoc """ @typedoc """
X coordinate module n in X coordinate module n in
@ -30,28 +25,6 @@ defmodule Explorer.Chain.Transaction do
""" """
@type s :: Decimal.t() @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 """ @typedoc """
The index of the transaction in its block. 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`. * `internal_transactions_indexed_at` - when `internal_transactions` were fetched by `Explorer.Indexer`.
* `logs` - events that occurred while mining the `transaction`. * `logs` - events that occurred while mining the `transaction`.
* `nonce` - the number of transaction made by the sender prior to this one * `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 * `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. 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 * `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. 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. * `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending.
* `to_address` - sink of `value` * `to_address` - sink of `value`
* `to_address_hash` - `to_address` foreign key * `to_address_hash` - `to_address` foreign key
@ -114,7 +85,7 @@ defmodule Explorer.Chain.Transaction do
block_number: Block.block_number() | nil, block_number: Block.block_number() | nil,
cumulative_gas_used: Gas.t() | nil, cumulative_gas_used: Gas.t() | nil,
from_address: %Ecto.Association.NotLoaded{} | Address.t(), from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_hash: Hash.Truncated.t(), from_address_hash: Hash.Address.t(),
gas: Gas.t(), gas: Gas.t(),
gas_price: wei_per_gas, gas_price: wei_per_gas,
gas_used: Gas.t() | nil, gas_used: Gas.t() | nil,
@ -125,13 +96,11 @@ defmodule Explorer.Chain.Transaction do
internal_transactions_indexed_at: DateTime.t(), internal_transactions_indexed_at: DateTime.t(),
logs: %Ecto.Association.NotLoaded{} | [Log.t()], logs: %Ecto.Association.NotLoaded{} | [Log.t()],
nonce: non_neg_integer(), nonce: non_neg_integer(),
public_key: public_key(),
r: r(), r: r(),
s: s(), s: s(),
standard_v: standard_v(),
status: Status.t() | nil, status: Status.t() | nil,
to_address: %Ecto.Association.NotLoaded{} | Address.t(), to_address: %Ecto.Association.NotLoaded{} | Address.t(),
to_address_hash: Hash.Truncated.t(), to_address_hash: Hash.Address.t(),
v: v(), v: v(),
value: Wei.t() value: Wei.t()
} }
@ -147,14 +116,12 @@ defmodule Explorer.Chain.Transaction do
field(:internal_transactions_indexed_at, :utc_datetime) field(:internal_transactions_indexed_at, :utc_datetime)
field(:input, Data) field(:input, Data)
field(:nonce, :integer) field(:nonce, :integer)
field(:public_key, Data)
field(:r, :decimal) field(:r, :decimal)
field(:s, :decimal) field(:s, :decimal)
field(:standard_v, :integer)
field(:status, Status) field(:status, Status)
field(:v, :integer) field(:v, :integer)
field(:value, Wei) field(:value, Wei)
field(:created_contract_address_hash, Hash.Truncated, virtual: true) field(:created_contract_address_hash, Hash.Address, virtual: true)
timestamps() timestamps()
@ -165,7 +132,7 @@ defmodule Explorer.Chain.Transaction do
Address, Address,
foreign_key: :from_address_hash, foreign_key: :from_address_hash,
references: :hash, references: :hash,
type: Hash.Truncated type: Hash.Address
) )
has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash) has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash)
@ -176,7 +143,7 @@ defmodule Explorer.Chain.Transaction do
Address, Address,
foreign_key: :to_address_hash, foreign_key: :to_address_hash,
references: :hash, references: :hash,
type: Hash.Truncated type: Hash.Address
) )
end end
@ -191,10 +158,8 @@ defmodule Explorer.Chain.Transaction do
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", ...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0, ...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> v: 0x8d, ...> v: 0x8d,
...> value: 0 ...> value: 0
...> } ...> }
@ -217,10 +182,8 @@ defmodule Explorer.Chain.Transaction do
...> index: 0, ...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0, ...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> status: :ok, ...> status: :ok,
...> v: 0x8d, ...> v: 0x8d,
...> value: 0 ...> value: 0
@ -254,10 +217,8 @@ defmodule Explorer.Chain.Transaction do
...> index: 0, ...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0, ...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> status: :ok, ...> status: :ok,
...> v: 0x8d, ...> v: 0x8d,
...> value: 0 ...> value: 0
@ -278,10 +239,8 @@ defmodule Explorer.Chain.Transaction do
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", ...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0, ...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75, ...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3, ...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> v: 0x8d, ...> v: 0x8d,
...> value: 0 ...> value: 0
...> } ...> }
@ -305,7 +264,6 @@ defmodule Explorer.Chain.Transaction do
|> cast(attrs, @required_attrs ++ @optional_attrs) |> cast(attrs, @required_attrs ++ @optional_attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> validate_collated_or_pending() |> validate_collated_or_pending()
|> validate_number(:standard_v, greater_than_or_equal_to: 0, less_than_or_equal_to: 3)
|> check_pending() |> check_pending()
|> check_collated() |> check_collated()
|> foreign_key_constraint(:block_hash) |> foreign_key_constraint(:block_hash)

@ -8,7 +8,7 @@ defmodule Explorer.Repo.Migrations.CreateBlocks do
add(:gas_used, :integer, null: false) add(:gas_used, :integer, null: false)
add(:hash, :bytea, null: false, primary_key: true) add(:hash, :bytea, null: false, primary_key: true)
add(:miner_hash, references(:addresses, column: :hash, type: :bytea), null: false) 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) add(:number, :bigint, null: false)
# not a foreign key to allow skipped blocks # not a foreign key to allow skipped blocks

@ -23,10 +23,8 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do
add(:internal_transactions_indexed_at, :utc_datetime, null: true) add(:internal_transactions_indexed_at, :utc_datetime, null: true)
add(:nonce, :integer, null: false) add(:nonce, :integer, null: false)
add(:public_key, :bytea, null: false)
add(:r, :numeric, precision: 100, null: false) add(:r, :numeric, precision: 100, null: false)
add(:s, :numeric, precision: 100, null: false) add(:s, :numeric, precision: 100, null: false)
add(:standard_v, :smallint, null: false)
# `null` when a pending transaction # `null` when a pending transaction
add(:status, :integer, null: true) 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, :block_hash))
create(index(:transactions, :from_address_hash)) create(index(:transactions, :from_address_hash))
create(index(:transactions, :to_address_hash)) create(index(:transactions, :to_address_hash))

@ -5,7 +5,10 @@ defmodule Explorer.Repo.Migrations.CreateLogs do
create table(:logs) do create table(:logs) do
add(:data, :bytea, null: false) add(:data, :bytea, null: false)
add(:index, :integer, 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(:first_topic, :string, null: true)
add(:second_topic, :string, null: true) add(:second_topic, :string, null: true)
add(:third_topic, :string, null: true) add(:third_topic, :string, null: true)

@ -0,0 +1,5 @@
defmodule Explorer.Chain.Hash.AddressTest do
use ExUnit.Case, async: true
doctest Explorer.Chain.Hash.Address
end

@ -0,0 +1,5 @@
defmodule Explorer.Chain.Hash.NonceTest do
use ExUnit.Case, async: true
doctest Explorer.Chain.Hash.Nonce
end

@ -1,5 +0,0 @@
defmodule Explorer.Chain.Hash.TruncatedTest do
use ExUnit.Case, async: true
doctest Explorer.Chain.Hash.Truncated
end

@ -16,10 +16,8 @@ defmodule Explorer.Chain.TransactionTest do
gas_price: 10000, gas_price: 10000,
input: "0x5c8eff12", input: "0x5c8eff12",
nonce: "31337", nonce: "31337",
public_key: "0xb39af9cb",
r: 0x9, r: 0x9,
s: 0x10, s: 0x10,
standard_v: 0x1,
transaction_index: "0x12", transaction_index: "0x12",
v: 27 v: 27
}) })

@ -65,7 +65,7 @@ defmodule Explorer.Factory do
{:ok, address_hash} = {:ok, address_hash} =
"address_hash" "address_hash"
|> sequence(& &1) |> sequence(& &1)
|> Hash.Truncated.cast() |> Hash.Address.cast()
address_hash address_hash
end end
@ -217,10 +217,6 @@ defmodule Explorer.Factory do
} }
end end
def public_key do
data(:public_key)
end
def market_history_factory do def market_history_factory do
%MarketHistory{ %MarketHistory{
closing_price: price(), closing_price: price(),
@ -258,10 +254,8 @@ defmodule Explorer.Factory do
hash: transaction_hash(), hash: transaction_hash(),
input: transaction_input(), input: transaction_input(),
nonce: Enum.random(1..1_000), nonce: Enum.random(1..1_000),
public_key: public_key(),
r: sequence(:transaction_r, & &1), r: sequence(:transaction_r, & &1),
s: sequence(:transaction_s, & &1), s: sequence(:transaction_s, & &1),
standard_v: Enum.random(0..3),
to_address: build(:address), to_address: build(:address),
v: Enum.random(27..30), v: Enum.random(27..30),
value: Enum.random(1..100_000) value: Enum.random(1..100_000)

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

@ -22,7 +22,7 @@ defmodule Indexer.AddressBalanceFetcher do
@doc """ @doc """
Asynchronously fetches balances for each address `hash` at the `block_number`. 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 :ok
def async_fetch_balances(address_fields) when is_list(address_fields) do def async_fetch_balances(address_fields) when is_list(address_fields) do
params_list = Enum.map(address_fields, &address_fields_to_params/1) params_list = Enum.map(address_fields, &address_fields_to_params/1)

@ -10,7 +10,6 @@ defmodule Indexer.BlockFetcher do
import Indexer, only: [debug: 1] import Indexer, only: [debug: 1]
alias EthereumJSONRPC alias EthereumJSONRPC
alias EthereumJSONRPC.Transactions
alias Explorer.Chain alias Explorer.Chain
alias Indexer.{AddressBalanceFetcher, AddressExtraction, BufferedTask, InternalTransactionFetcher, Sequence} 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, []), do: {:ok, %{logs: [], receipts: []}}
defp fetch_transaction_receipts(%{} = state, hashes) do defp fetch_transaction_receipts(%{} = state, transaction_params) do
debug(fn -> "fetching #{length(hashes)} transaction receipts" end) debug(fn -> "fetching #{length(transaction_params)} transaction receipts" end)
stream_opts = [max_concurrency: state.receipts_concurrency, timeout: :infinity] stream_opts = [max_concurrency: state.receipts_concurrency, timeout: :infinity]
hashes transaction_params
|> Enum.chunk_every(state.receipts_batch_size) |> Enum.chunk_every(state.receipts_batch_size)
|> Task.async_stream(&EthereumJSONRPC.fetch_transaction_receipts(&1), stream_opts) |> Task.async_stream(&EthereumJSONRPC.fetch_transaction_receipts(&1), stream_opts)
|> Enum.reduce_while({:ok, %{logs: [], receipts: []}}, fn |> 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)}, with {:blocks, {:ok, next, result}} <- {:blocks, EthereumJSONRPC.fetch_blocks_by_range(range)},
%{blocks: blocks, transactions: transactions_without_receipts} = result, %{blocks: blocks, transactions: transactions_without_receipts} = result,
cap_seq(seq, next, range), cap_seq(seq, next, range),
transaction_hashes = Transactions.params_to_hashes(transactions_without_receipts), {:receipts, {:ok, receipt_params}} <-
{:receipts, {:ok, receipt_params}} <- {:receipts, fetch_transaction_receipts(state, transaction_hashes)}, {:receipts, fetch_transaction_receipts(state, transactions_without_receipts)},
%{logs: logs, receipts: receipts} = receipt_params, %{logs: logs, receipts: receipts} = receipt_params,
transactions_with_receipts = put_receipts(transactions_without_receipts, receipts) do transactions_with_receipts = put_receipts(transactions_without_receipts, receipts) do
addresses = addresses =

@ -125,6 +125,9 @@ defmodule Indexer.InternalTransactionFetcher do
# re-queue the de-duped transactions_params # re-queue the de-duped transactions_params
{:retry, unique_transactions_params} {:retry, unique_transactions_params}
:ignore ->
:ok
end end
end end

@ -9,7 +9,7 @@ defmodule Indexer.PendingTransactionFetcher do
require Logger require Logger
import EthereumJSONRPC.Parity, only: [fetch_pending_transactions: 0] import EthereumJSONRPC, only: [fetch_pending_transactions: 0]
alias Explorer.Chain alias Explorer.Chain
alias Indexer.{AddressExtraction, PendingTransactionFetcher} alias Indexer.{AddressExtraction, PendingTransactionFetcher}
@ -85,17 +85,21 @@ defmodule Indexer.PendingTransactionFetcher do
end end
defp task(%PendingTransactionFetcher{} = _state) do defp task(%PendingTransactionFetcher{} = _state) do
{:ok, transactions_params} = fetch_pending_transactions() case fetch_pending_transactions() do
{:ok, transactions_params} ->
addresses_params = AddressExtraction.extract_addresses(%{transactions: transactions_params}, pending: true) 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 # 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 # affected the address balance yet since address balance is a balance at a give block and these transactions are
# blockless. # blockless.
{:ok, _} = {:ok, _} =
Chain.import_blocks( Chain.import_blocks(
addresses: [params: addresses_params], addresses: [params: addresses_params],
transactions: [on_conflict: :nothing, params: transactions_params] transactions: [on_conflict: :nothing, params: transactions_params]
) )
:ignore ->
:ok
end
end end
end end

@ -6,23 +6,37 @@ defmodule Indexer.AddressBalanceFetcherTest do
alias Explorer.Chain.{Address, Hash, Wei} alias Explorer.Chain.{Address, Hash, Wei}
alias Indexer.{AddressBalanceFetcher, AddressBalanceFetcherCase} 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 setup do
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
:ok %{variant: EthereumJSONRPC.config(:variant)}
end end
describe "init/1" do describe "init/1" do
test "fetches unfetched Block miner balance" do test "fetches unfetched Block miner balance", %{variant: variant} do
{:ok, miner_hash} = Hash.Truncated.cast("0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca") %{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) 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 == nil
assert miner.fetched_balance_block_number == nil assert miner.fetched_balance_block_number == nil
@ -36,14 +50,34 @@ defmodule Indexer.AddressBalanceFetcherTest do
) )
end) 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 assert fetched_address.fetched_balance_block_number == block.number
end end
test "fetches unfetched addresses when less than max batch size" do test "fetches unfetched addresses when less than max batch size", %{variant: variant} do
{:ok, miner_hash} = Hash.Truncated.cast("0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca") %{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) 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) AddressBalanceFetcherCase.start_supervised!(max_batch_size: 2)
@ -54,43 +88,93 @@ defmodule Indexer.AddressBalanceFetcherTest do
) )
end) 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 assert fetched_address.fetched_balance_block_number == block.number
end end
end end
describe "async_fetch_balances/1" do 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!() 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 = address =
wait(fn -> wait(fn ->
Repo.get!(Address, @hash) Repo.get!(Address, hash)
end) end)
assert address.fetched_balance == %Wei{value: Decimal.new(1)} assert address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)}
assert address.fetched_balance_block_number == @block_number assert address.fetched_balance_block_number == block_number
end end
end end
describe "run/2" do describe "run/2" do
test "duplicate address hashes the max block_quantity" do @tag capture_log: true
hash_data = "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" test "duplicate address hashes the max block_quantity", %{variant: variant} do
%{fetched_balance: fetched_balance, hash_data: hash_data} =
assert AddressBalanceFetcher.run( case variant do
[%{block_quantity: "0x1", hash_data: hash_data}, %{block_quantity: "0x2", hash_data: hash_data}], EthereumJSONRPC.Geth ->
0 %{
) == :ok fetched_balance: 5_000_000_000_000_000_000,
hash_data: "0x05a56e2d52c817161883f50c441c3228cfe54d9f"
fetched_address = Repo.one!(from(address in Address, where: address.hash == ^hash_data)) }
assert fetched_address.fetched_balance == %Explorer.Chain.Wei{ EthereumJSONRPC.Parity ->
value: Decimal.new(252_460_802_000_000_000_000_000_000) %{
} fetched_balance: 252_460_802_000_000_000_000_000_000,
hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
assert fetched_address.fetched_balance_block_number == 2 }
_ ->
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 end
test "duplicate address hashes only retry max block_quantity" do test "duplicate address hashes only retry max block_quantity" do

@ -3,6 +3,7 @@ defmodule Indexer.BlockFetcherTest do
use Explorer.DataCase, async: false use Explorer.DataCase, async: false
import ExUnit.CaptureLog import ExUnit.CaptureLog
import EthereumJSONRPC.Case
alias Explorer.Chain.{Address, Block, Log, Transaction, Wei} alias Explorer.Chain.{Address, Block, Log, Transaction, Wei}
@ -36,6 +37,10 @@ defmodule Indexer.BlockFetcherTest do
# ON blocks.hash = transactions.block_hash) as blocks # ON blocks.hash = transactions.block_hash) as blocks
@first_full_block_number 37 @first_full_block_number 37
setup do
%{variant: EthereumJSONRPC.config(:variant)}
end
describe "start_link/1" do describe "start_link/1" do
test "starts fetching blocks from latest and goes down" do test "starts fetching blocks from latest and goes down" do
{:ok, latest_block_number} = EthereumJSONRPC.fetch_block_number_by_tag("latest") {:ok, latest_block_number} = EthereumJSONRPC.fetch_block_number_by_tag("latest")
@ -137,104 +142,231 @@ defmodule Indexer.BlockFetcherTest do
%{state: state} %{state: state}
end 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) {:ok, sequence} = Sequence.start_link([], 0, 1)
assert {:ok, %{address_hash: address_hash, block_hash: block_hash} =
%{ case variant do
addresses: [ EthereumJSONRPC.Geth ->
%Explorer.Chain.Hash{ %{
byte_count: 20, address_hash: %Explorer.Chain.Hash{
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> byte_count: 20,
} = address_hash bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
], },
blocks: [ block_hash: %Explorer.Chain.Hash{
%Explorer.Chain.Hash{ byte_count: 32,
byte_count: 32, bytes:
bytes: <<212, 229, 103, 64, 248, 118, 174, 248, 192, 16, 184, 106, 64, 213, 245, 103, 69, 161, 24, 208, 144,
<<91, 40, 193, 191, 211, 161, 82, 48, 201, 164, 107, 57, 156, 208, 249, 166, 146, 13, 67, 46, 133, 106, 52, 230, 154, 236, 140, 13, 177, 203, 143, 163>>
56, 28, 198, 161, 64, 176, 110, 132, 16, 17, 47>> }
} }
],
logs: [], EthereumJSONRPC.Parity ->
transactions: [] %{
}} = BlockFetcher.import_range(0..0, state, sequence) 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(InternalTransactionFetcher)
wait_for_tasks(AddressBalanceFetcher) wait_for_tasks(AddressBalanceFetcher)
assert Repo.aggregate(Block, :count, :hash) == 1 assert Repo.aggregate(Block, :count, :hash) == 1
assert Repo.aggregate(Address, :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 == %Wei{value: Decimal.new(0)}
assert address.fetched_balance_block_number == 0 assert address.fetched_balance_block_number == 0
end
)
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) {:ok, sequence} = Sequence.start_link([], 0, 1)
assert {:ok, case variant do
%{ EthereumJSONRPC.Geth ->
addresses: [ block_number = 48230
%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) assert {:ok,
wait_for_tasks(AddressBalanceFetcher) %{
addresses: [
assert Repo.aggregate(Block, :count, :hash) == 1 %Explorer.Chain.Hash{
assert Repo.aggregate(Address, :count, :hash) == 2 byte_count: 20,
assert Repo.aggregate(Log, :count, :id) == 1 bytes:
assert Repo.aggregate(Transaction, :count, :hash) == 1 <<55, 52, 203, 24, 116, 145, 237, 231, 19, 174, 91, 59, 45, 18, 40, 74, 244, 107, 129, 1>>
} = first_address_hash,
first_address = Repo.get!(Address, first_address_hash) %Explorer.Chain.Hash{
byte_count: 20,
assert first_address.fetched_balance == %Wei{value: Decimal.new(1)} bytes:
assert first_address.fetched_balance_block_number == @first_full_block_number <<89, 47, 120, 202, 98, 102, 132, 20, 109, 56, 18, 133, 202, 0, 221, 145, 179, 117, 253, 17>>
} = second_address_hash,
second_address = Repo.get!(Address, second_address_hash) %Explorer.Chain.Hash{
byte_count: 20,
assert second_address.fetched_balance == %Wei{value: Decimal.new(252_460_837_000_000_000_000_000_000)} bytes:
assert second_address.fetched_balance_block_number == @first_full_block_number <<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
end end
@ -292,7 +424,7 @@ defmodule Indexer.BlockFetcherTest do
end end
defp wait_for_tasks(buffered_task) do defp wait_for_tasks(buffered_task) do
wait_until(5000, fn -> wait_until(10_000, fn ->
counts = BufferedTask.debug_count(buffered_task) counts = BufferedTask.debug_count(buffered_task)
counts.buffer == 0 and counts.tasks == 0 counts.buffer == 0 and counts.tasks == 0
end) end)

@ -3,18 +3,18 @@ defmodule Indexer.InternalTransactionFetcherTest do
import ExUnit.CaptureLog import ExUnit.CaptureLog
alias Explorer.Chain.Transaction alias Indexer.{AddressBalanceFetcherCase, InternalTransactionFetcher}
alias Indexer.{AddressBalanceFetcherCase, InternalTransactionFetcher, PendingTransactionFetcher}
@moduletag :capture_log @moduletag :capture_log
@tag :no_geth
test "does not try to fetch pending transactions from Indexer.PendingTransactionFetcher" do test "does not try to fetch pending transactions from Indexer.PendingTransactionFetcher" do
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
AddressBalanceFetcherCase.start_supervised!() AddressBalanceFetcherCase.start_supervised!()
start_supervised!(PendingTransactionFetcher) start_supervised!(Indexer.PendingTransactionFetcher)
wait_for_results(fn -> 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) end)
:transaction :transaction
@ -81,6 +81,7 @@ defmodule Indexer.InternalTransactionFetcherTest do
""" """
end end
@tag :no_geth
test "duplicate transaction hashes only retry uniques" do test "duplicate transaction hashes only retry uniques" do
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
AddressBalanceFetcherCase.start_supervised!() AddressBalanceFetcherCase.start_supervised!()

@ -2,12 +2,13 @@ defmodule Indexer.PendingTransactionFetcherTest do
# `async: false` due to use of named GenServer # `async: false` due to use of named GenServer
use Explorer.DataCase, async: false use Explorer.DataCase, async: false
alias Explorer.Chain.Transaction
alias Indexer.PendingTransactionFetcher
describe "start_link/1" do 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 # this test may fail if Sokol so low volume that the pending transactions are empty for too long
test "starts fetching pending transactions" do test "starts fetching pending transactions" do
alias Explorer.Chain.Transaction
alias Indexer.PendingTransactionFetcher
assert Repo.aggregate(Transaction, :count, :hash) == 0 assert Repo.aggregate(Transaction, :count, :hash) == 0
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})

@ -1,7 +1,7 @@
{ {
"coverage_options": { "coverage_options": {
"treat_no_relevant_lines_as_covered": true, "treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 94.4 "minimum_coverage": 94.5
}, },
"terminal_options": { "terminal_options": {
"file_column_width": 120 "file_column_width": 120

Loading…
Cancel
Save