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
command: mix sobelow --config
working_directory: "apps/explorer_web"
test:
test_geth:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.6.5-node-browsers
@ -306,6 +306,7 @@ jobs:
PGPASSWORD: postgres
# match POSTGRES_USER for postgres image below
PGUSER: postgres
ETHEREUM_JSONRPC_VARIANT: geth
- image: circleci/postgres:10.3-alpine
environment:
# Match apps/explorer/config/test.exs config :explorerer, Explorer.Repo, database
@ -328,7 +329,44 @@ jobs:
name: Wait for DB
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run: mix coveralls.circle --umbrella
- run: mix coveralls.circle --exclude no_geth --parallel --umbrella
- store_test_results:
path: _build/test/junit
test_parity:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.6.5-node-browsers
environment:
MIX_ENV: test
# match POSTGRES_PASSWORD for postgres image below
PGPASSWORD: postgres
# match POSTGRES_USER for postgres image below
PGUSER: postgres
ETHEREUM_JSONRPC_VARIANT: parity
- image: circleci/postgres:10.3-alpine
environment:
# Match apps/explorer/config/test.exs config :explorerer, Explorer.Repo, database
POSTGRES_DB: explorer_test
# match PGPASSWORD for elixir image above
POSTGRES_PASSWORD: postgres
# match PGUSER for elixir image above
POSTGRES_USER: postgres
working_directory: ~/app
steps:
- attach_workspace:
at: .
- run: mix local.hex --force
- run: mix local.rebar --force
- run:
name: Wait for DB
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run: mix coveralls.circle --parallel --umbrella
- store_test_results:
path: _build/test/junit
@ -356,7 +394,8 @@ workflows:
- eslint
- jest
- sobelow
- test
- test_parity
- test_geth
- dialyzer:
requires:
- build
@ -372,6 +411,9 @@ workflows:
- sobelow:
requires:
- build
- test:
- test_parity:
requires:
- build
- test_geth:
requires:
- build

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

@ -3,6 +3,10 @@
use Mix.Config
config :ethereum_jsonrpc,
http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]],
trace_url: "https://sokol-trace.poa.network",
url: "https://sokol.poa.network"
http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
variant = System.get_env("ETHEREUM_JSONRPC_VARIANT") || "parity"
# Import variant specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{variant}.exs"

@ -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
alias Explorer.Chain.Block
alias EthereumJSONRPC.{Blocks, Parity, Receipts, Transactions}
alias EthereumJSONRPC.{Blocks, Receipts, Transactions}
@typedoc """
Truncated 20-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a
@ -89,6 +89,27 @@ defmodule EthereumJSONRPC do
Application.fetch_env!(:ethereum_jsonrpc, key)
end
@doc """
Fetches the configured url for the specific `method` or the fallback `url`
Configuration for a specific `method` can be set in `method_to_url` `config`
config :ethereum_jsonrpc,
method_to_url: [
eth_getBalance: "method_to_url"
]
The fallback 'url' MUST we set if not all methods have a url set.
config :ethereum_jsonrpc,
url:
"""
def method_to_url(method) when is_atom(method) do
:ethereum_jsonrpc
|> Application.get_env(:method_to_url, [])
|> Keyword.get_lazy(method, fn -> config(:url) end)
end
@doc """
Fetches balance for each address `hash` at the `block_number`
"""
@ -108,7 +129,7 @@ defmodule EthereumJSONRPC do
with {:ok, responses} <-
id_to_params
|> get_balance_requests()
|> json_rpc(config(:trace_url)) do
|> json_rpc(method_to_url(:eth_getBalance)) do
get_balance_responses_to_addresses_params(responses, id_to_params)
end
end
@ -121,7 +142,7 @@ defmodule EthereumJSONRPC do
def fetch_blocks_by_hash(block_hashes) do
block_hashes
|> get_block_by_hash_requests()
|> json_rpc(config(:url))
|> json_rpc(method_to_url(:eth_getBlockByHash))
|> handle_get_blocks()
|> case do
{:ok, _next, results} -> {:ok, results}
@ -135,18 +156,13 @@ defmodule EthereumJSONRPC do
def fetch_blocks_by_range(_first.._last = range) do
range
|> get_block_by_number_requests()
|> json_rpc(config(:url))
|> json_rpc(method_to_url(:eth_getBlockByNumber))
|> handle_get_blocks()
end
@doc """
Fetches block number by `t:tag/0`.
The `"earliest"` tag is the earlist block number, which is `0`.
iex> EthereumJSONRPC.fetch_block_number_by_tag("earliest")
{:ok, 0}
## Returns
* `{:ok, number}` - the block number for the given `tag`.
@ -158,19 +174,29 @@ defmodule EthereumJSONRPC do
def fetch_block_number_by_tag(tag) when tag in ~w(earliest latest pending) do
tag
|> get_block_by_tag_request()
|> json_rpc(config(:url))
|> json_rpc(method_to_url(:eth_getBlockByNumber))
|> handle_get_block_by_tag()
end
@doc """
Fetches internal transactions from client-specific API.
Fetches internal transactions from variant API.
"""
def fetch_internal_transactions(params_list) when is_list(params_list) do
Parity.fetch_internal_transactions(params_list)
config(:variant).fetch_internal_transactions(params_list)
end
def fetch_transaction_receipts(hashes) when is_list(hashes) do
Receipts.fetch(hashes)
@doc """
Fetches pending transactions from variant API.
"""
def fetch_pending_transactions do
config(:variant).fetch_pending_transactions()
end
@spec fetch_transaction_receipts([
%{required(:gas) => non_neg_integer(), required(:hash) => hash, optional(atom) => any}
]) :: {:ok, %{logs: list(), receipts: list()}} | {:error, reason :: term}
def fetch_transaction_receipts(transactions_params) when is_list(transactions_params) do
Receipts.fetch(transactions_params)
end
@doc """
@ -200,8 +226,14 @@ defmodule EthereumJSONRPC do
json = encode_json(payload)
case post(url, json, config(:http)) do
{:ok, %HTTPoison.Response{body: body, status_code: code}} ->
body |> decode_json(code, json, url) |> handle_response(code)
{:ok, %HTTPoison.Response{body: body, status_code: status_code}} ->
with {:ok, json} <-
decode_json(
request: [url: url, body: json],
response: [status_code: status_code, body: body]
) do
handle_response(json, status_code)
end
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
@ -272,8 +304,10 @@ defmodule EthereumJSONRPC do
rechunk_json_rpc(url, chunks, options, response, decoded_response_bodies)
{:ok, %HTTPoison.Response{body: body, status_code: status_code}} ->
decoded_body = decode_json(body, status_code, json, url)
chunked_json_rpc(url, tail, options, [decoded_body | decoded_response_bodies])
with {:ok, decoded_body} <-
decode_json(request: [url: url, body: json], response: [status_code: status_code, body: body]) do
chunked_json_rpc(url, tail, options, [decoded_body | decoded_response_bodies])
end
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
@ -380,7 +414,7 @@ defmodule EthereumJSONRPC do
defp get_block_by_tag_request(tag) do
# eth_getBlockByNumber accepts either a number OR a tag
get_block_by_number_request(%{id: tag, tag: tag, transactions: :hashes})
get_block_by_number_request(%{id: 1, tag: tag, transactions: :hashes})
end
defp get_block_by_number_params(options) do
@ -412,25 +446,25 @@ defmodule EthereumJSONRPC do
defp encode_json(data), do: Jason.encode_to_iodata!(data)
defp decode_json(response_body, response_status_code, request_body, request_url) do
Jason.decode!(response_body)
rescue
Jason.DecodeError ->
Logger.error(fn ->
"""
failed to decode json payload:
request url: #{inspect(request_url)}
defp decode_json(named_arguments) when is_list(named_arguments) do
response = Keyword.fetch!(named_arguments, :response)
response_body = Keyword.fetch!(response, :body)
request body: #{inspect(request_body)}
with {:error, _} <- Jason.decode(response_body) do
case Keyword.fetch!(response, :status_code) do
# CloudFlare protected server return HTML errors for 502, so the JSON decode will fail
502 ->
request_url =
named_arguments
|> Keyword.fetch!(:request)
|> Keyword.fetch!(:url)
response status code: #{inspect(response_status_code)}
{:error, {:bad_gateway, request_url}}
response body: #{inspect(response_body)}
"""
end)
raise("bad jason")
_ ->
raise EthereumJSONRPC.DecodeError, named_arguments
end
end
end
defp handle_get_blocks({:ok, results}) do

@ -4,7 +4,7 @@ defmodule EthereumJSONRPC.Block do
and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber).
"""
import EthereumJSONRPC, only: [nonce_to_integer: 1, quantity_to_integer: 1, timestamp_to_datetime: 1]
import EthereumJSONRPC, only: [quantity_to_integer: 1, timestamp_to_datetime: 1]
alias EthereumJSONRPC
alias EthereumJSONRPC.Transactions
@ -22,6 +22,8 @@ defmodule EthereumJSONRPC.Block do
for the logs of the block. `nil` when block is pending.
* `"miner"` - `t:EthereumJSONRPC.address/0` of the beneficiary to whom the mining rewards were given. Aliased by
`"author"`.
* `"mixHash"` - Generated from [DAG](https://ethereum.stackexchange.com/a/10353) as part of Proof-of-Work for EthHash
algorithm. **[Geth](https://github.com/ethereum/go-ethereum/wiki/geth) + Proof-of-Work-only**
* `"nonce"` - `t:EthereumJSONRPC.nonce/0`. `nil` when its pending block.
* `"number"` - the block number `t:EthereumJSONRPC.quantity/0`. `nil` when block is pending.
* `"parentHash" - the `t:EthereumJSONRPC.hash/0` of the parent block.
@ -92,11 +94,50 @@ defmodule EthereumJSONRPC.Block do
total_difficulty: 340282366920938463463374607431465668165
}
[Geth] `elixir` can be converted to params
iex> EthereumJSONRPC.Block.elixir_to_params(
...> %{
...> "difficulty" => 17561410778,
...> "extraData" => "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32",
...> "gasLimit" => 5000,
...> "gasUsed" => 0,
...> "hash" => "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xbb7b8287f3f0a933474a79eae42cbca977791171",
...> "mixHash" => "0xbbb93d610b2b0296a59f18474ac3d6086a9902aa7ca4b9a306692f7c3d496fdf",
...> "nonce" => 5539500215739777653,
...> "number" => 59,
...> "parentHash" => "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e",
...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "size" => 542,
...> "stateRoot" => "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba",
...> "timestamp" => Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 1039309006117,
...> "transactions" => [],
...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "uncles" => []
...> }
...> )
%{
difficulty: 17561410778,
gas_limit: 5000,
gas_used: 0,
hash: "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623",
miner_hash: "0xbb7b8287f3f0a933474a79eae42cbca977791171",
nonce: 5539500215739777653,
number: 59,
parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e",
size: 542,
timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"),
total_difficulty: 1039309006117
}
"""
@spec elixir_to_params(elixir) :: map
def elixir_to_params(
%{
"author" => miner_hash,
"difficulty" => difficulty,
"gasLimit" => gas_limit,
"gasUsed" => gas_used,
@ -281,14 +322,10 @@ defmodule EthereumJSONRPC.Block do
# `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct
# hash format
defp entry_to_elixir({key, _} = entry)
when key in ~w(author extraData hash logsBloom miner parentHash receiptsRoot sealFields sha3Uncles signature
stateRoot step transactionsRoot uncles),
when key in ~w(author extraData hash logsBloom miner mixHash nonce parentHash receiptsRoot sealFields sha3Uncles
signature stateRoot step transactionsRoot uncles),
do: entry
defp entry_to_elixir({"nonce" = key, nonce}) do
{key, nonce_to_integer(nonce)}
end
defp entry_to_elixir({"timestamp" = key, timestamp}) do
{key, timestamp_to_datetime(timestamp)}
end

@ -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"
}
Geth does not supply a `"type"`
iex> EthereumJSONRPC.Log.elixir_to_params(
...> %{
...> "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55",
...> "blockHash" => "0x0b89f7f894f5d8ba941e16b61490e999a0fcaaf92dfcc70aee2ac5ddb5f243e1",
...> "blockNumber" => 4448,
...> "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",
...> "logIndex" => 0,
...> "removed" => false,
...> "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130",
...> "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a",
...> "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"],
...> "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e",
...> "transactionIndex" => 0
...> }
...> )
%{
address_hash: "0xda8b3276cde6d768a44b9dac659faa339a41ac55",
block_number: 4448,
data: "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",
first_topic: "0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130",
fourth_topic: nil,
index: 0,
second_topic: "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a",
third_topic: "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed",
transaction_hash: "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e"
}
"""
def elixir_to_params(%{
"address" => address_hash,
"blockNumber" => block_number,
"data" => data,
"logIndex" => index,
"topics" => topics,
"transactionHash" => transaction_hash,
"type" => type
}) do
def elixir_to_params(
%{
"address" => address_hash,
"blockNumber" => block_number,
"data" => data,
"logIndex" => index,
"topics" => topics,
"transactionHash" => transaction_hash
} = elixir
) do
%{
address_hash: address_hash,
block_number: block_number,
data: data,
index: index,
transaction_hash: transaction_hash,
type: type
transaction_hash: transaction_hash
}
|> put_topics(topics)
|> put_type(elixir)
end
@doc """
@ -101,6 +131,37 @@ defmodule EthereumJSONRPC.Log do
"type" => "mined"
}
Geth and Parity >= 1.11.4 includes a `"removed"` key
iex> EthereumJSONRPC.Log.to_elixir(
...> %{
...> "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55",
...> "blockHash" => "0x0b89f7f894f5d8ba941e16b61490e999a0fcaaf92dfcc70aee2ac5ddb5f243e1",
...> "blockNumber" => "0x1160",
...> "data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",
...> "logIndex" => "0x0",
...> "removed" => false,
...> "topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130",
...> "0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a",
...> "0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"],
...> "transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e",
...> "transactionIndex" => "0x0"
...> }
...> )
%{
"address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55",
"blockHash" => "0x0b89f7f894f5d8ba941e16b61490e999a0fcaaf92dfcc70aee2ac5ddb5f243e1",
"blockNumber" => 4448,
"data" => "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",
"logIndex" => 0,
"removed" => false,
"topics" => ["0xadc1e8a294f8415511303acc4a8c0c5906c7eb0bf2a71043d7f4b03b46a39130",
"0x000000000000000000000000c15bf627accd3b054075c7880425f903106be72a",
"0x000000000000000000000000a59eb37750f9c8f2e11aac6700e62ef89187e4ed"],
"transactionHash" => "0xf9b663b4e9b1fdc94eb27b5cfba04eb03d2f7b3fa0b24eb2e1af34f823f2b89e",
"transactionIndex" => 0
}
"""
def to_elixir(log) when is_map(log) do
Enum.into(log, %{}, &entry_to_elixir/1)
@ -120,4 +181,10 @@ defmodule EthereumJSONRPC.Log do
|> Map.put(:third_topic, Enum.at(topics, 2))
|> Map.put(:fourth_topic, Enum.at(topics, 3))
end
defp put_type(params, %{"type" => type}) do
Map.put(params, :type, type)
end
defp put_type(params, _), do: params
end

@ -3,21 +3,24 @@ defmodule EthereumJSONRPC.Parity do
Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/).
"""
import EthereumJSONRPC, only: [config: 1, id_to_params: 1, json_rpc: 2, request: 1]
import EthereumJSONRPC, only: [id_to_params: 1, method_to_url: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Parity.Traces
alias EthereumJSONRPC.{Transaction, Transactions}
@behaviour EthereumJSONRPC.Variant
@doc """
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Parity trace URL.
"""
@impl EthereumJSONRPC.Variant
def fetch_internal_transactions(transactions_params) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params)
with {:ok, responses} <-
id_to_params
|> trace_replay_transaction_requests()
|> json_rpc(config(:trace_url)) do
|> json_rpc(method_to_url(:trace_replayTransaction)) do
trace_replay_transaction_responses_to_internal_transactions_params(responses, id_to_params)
end
end
@ -28,12 +31,13 @@ defmodule EthereumJSONRPC.Parity do
*NOTE*: The pending transactions are local to the node that is contacted and may not be consistent across nodes based
on the transactions that each node has seen and how each node prioritizes collating transactions into the next block.
"""
@impl EthereumJSONRPC.Variant
@spec fetch_pending_transactions() :: {:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions do
with {:ok, transactions} <-
%{id: 1, method: "parity_pendingTransactions", params: []}
|> request()
|> json_rpc(config(:url)) do
|> json_rpc(method_to_url(:parity_pendingTransactions)) do
transactions_params =
transactions
|> Transactions.to_elixir()

@ -19,12 +19,14 @@ defmodule EthereumJSONRPC.Receipt do
* `"blockNumber"` - The block number `t:EthereumJSONRPC.quanity/0`.
* `"cumulativeGasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used when this transaction was executed in the
block.
* `"from"` - The `EthereumJSONRPC.Transaction.t/0` `"from"` address hash. **Geth-only.**
* `"gasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used by this specific transaction alone.
* `"logs"` - `t:list/0` of log objects, which this transaction generated.
* `"logsBloom"` - `t:EthereumJSONRPC.data/0` of 256 Bytes for
[Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) for light clients to quickly retrieve related logs.
* `"root"` - `t:EthereumJSONRPC.hash/0` of post-transaction stateroot (pre-Byzantium)
* `"status"` - `t:EthereumJSONRPC.quantity/0` of either 1 (success) or 0 (failure) (post-Byzantium)
* `"to"` - The `EthereumJSONRPC.Transaction.t/0` `"to"` address hash. **Geth-only.**
* `"transactionHash"` - `t:EthereumJSONRPC.hash/0` the transaction.
* `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the transaction index in the block.
"""
@ -70,6 +72,87 @@ defmodule EthereumJSONRPC.Receipt do
transaction_index: 0
}
Geth, when showing pre-[Byzantium](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-609.md) does not include
the [status](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-658.md) as that was a post-Byzantium
[EIP](https://github.com/ethereum/EIPs/tree/master/EIPS).
Pre-Byzantium receipts are given a derived `:status`:
* If `"gas"` (supplied by caller from `EthereumJSONRPC.Transaction.elixir`) `==` `"gasUsed"`, then `:status` is
`:error`
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 21000,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gas" => 21000,
...> "gasUsed" => 21000,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => 0
...> }
...> )
%{
cumulative_gas_used: 21000,
gas_used: 21000,
status: :error,
transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
transaction_index: 0
}
* Otherwise, `:status` is `:ok`
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 21000,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gas" => 40000,
...> "gasUsed" => 21000,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => 0
...> }
...> )
%{
cumulative_gas_used: 21000,
gas_used: 21000,
status: :ok,
transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
transaction_index: 0
}
It is a developer error if the budgeted `"gas"` is not supplied for deriving the pre-Byzantium `:status`.
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 21000,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gasUsed" => 21000,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => 0
...> }
...> )
** (ArgumentError) Pre-Byzantium transaction receipts require the transaction gas to be given to derive their status
"""
@spec elixir_to_params(elixir) :: %{
cumulative_gas_used: non_neg_integer,
@ -78,13 +161,16 @@ defmodule EthereumJSONRPC.Receipt do
transaction_hash: String.t(),
transaction_index: non_neg_integer()
}
def elixir_to_params(%{
"cumulativeGasUsed" => cumulative_gas_used,
"gasUsed" => gas_used,
"status" => status,
"transactionHash" => transaction_hash,
"transactionIndex" => transaction_index
}) do
def elixir_to_params(
%{
"cumulativeGasUsed" => cumulative_gas_used,
"gasUsed" => gas_used,
"transactionHash" => transaction_hash,
"transactionIndex" => transaction_index
} = elixir
) do
status = elixir_to_status(elixir)
%{
cumulative_gas_used: cumulative_gas_used,
gas_used: gas_used,
@ -126,19 +212,77 @@ defmodule EthereumJSONRPC.Receipt do
"transactionIndex" => 0
}
Receipts from Geth also supply the `EthereumJSONRPC.Transaction.t/0` `"from"` and `"to"` address hashes.
iex> EthereumJSONRPC.Receipt.to_elixir(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => "0xb443",
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => "0x5208",
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gasUsed" => "0x5208",
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => "0x0"
...> }
...> )
%{
"blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"blockNumber" => 46147,
"contractAddress" => nil,
"cumulativeGasUsed" => 21000,
"from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
"gasUsed" => 21000,
"logs" => [],
"logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
"to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
"transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
"transactionIndex" => 0
}
"""
@spec to_elixir(t) :: elixir
def to_elixir(receipt) when is_map(receipt) do
Enum.into(receipt, %{}, &entry_to_elixir/1)
end
defp elixir_to_status(elixir) do
case elixir do
%{"status" => status} ->
status
%{"gas" => gas, "gasUsed" => gas_used} ->
pre_byzantium_status(gas, gas_used)
_ ->
raise ArgumentError,
"Pre-Byzantium transaction receipts require the transaction gas to be given to derive their status"
end
end
defp pre_byzantium_status(gas, gas_used) when is_integer(gas) and is_integer(gas_used) do
if gas_used < gas do
:ok
else
:error
end
end
# double check that no new keys are being missed by requiring explicit match for passthrough
# `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct
# hash format
defp entry_to_elixir({key, _} = entry) when key in ~w(blockHash contractAddress logsBloom root transactionHash),
do: entry
# gas is passsed in from the `t:EthereumJSONRPC.Transaction.params/0` to allow pre-Byzantium status to be derived
defp entry_to_elixir({key, _} = entry)
when key in ~w(blockHash contractAddress from gas logsBloom root to transactionHash),
do: entry
defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex) do
defp entry_to_elixir({key, quantity})
when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex) do
{key, quantity_to_integer(quantity)}
end

@ -111,15 +111,31 @@ defmodule EthereumJSONRPC.Receipts do
Enum.map(elixir, &Receipt.elixir_to_params/1)
end
def fetch(hashes) when is_list(hashes) do
hashes
|> Enum.map(&hash_to_json/1)
@spec fetch([
%{
required(:gas) => non_neg_integer(),
required(:hash) => EthereumJSONRPC.hash(),
optional(atom) => any
}
]) :: {:ok, %{logs: list(), receipts: list()}} | {:error, reason :: term}
def fetch(transactions_params) when is_list(transactions_params) do
{requests, id_to_transaction_params} =
transactions_params
|> Stream.with_index()
|> Enum.reduce({[], %{}}, fn {%{hash: transaction_hash} = transaction_params, id},
{acc_requests, acc_id_to_transaction_params} ->
requests = [request(id, transaction_hash) | acc_requests]
id_to_transaction_params = Map.put(acc_id_to_transaction_params, id, transaction_params)
{requests, id_to_transaction_params}
end)
requests
|> json_rpc(config(:url))
|> case do
{:ok, responses} ->
elixir_receipts =
responses
|> responses_to_receipts()
|> responses_to_receipts(id_to_transaction_params)
|> to_elixir()
elixir_logs = elixir_to_logs(elixir_receipts)
@ -199,18 +215,29 @@ defmodule EthereumJSONRPC.Receipts do
Enum.map(receipts, &Receipt.to_elixir/1)
end
defp hash_to_json(hash) do
defp request(id, transaction_hash) when is_integer(id) and is_binary(transaction_hash) do
%{
"id" => hash,
"id" => id,
"jsonrpc" => "2.0",
"method" => "eth_getTransactionReceipt",
"params" => [hash]
"params" => [transaction_hash]
}
end
defp response_to_receipt(%{"result" => receipt}), do: receipt
defp response_to_receipt(%{"result" => nil}, _), do: %{}
defp responses_to_receipts(responses) when is_list(responses) do
Enum.map(responses, &response_to_receipt/1)
defp response_to_receipt(%{"id" => id, "result" => receipt}, id_to_transaction_params) do
gas =
id_to_transaction_params
|> Map.fetch!(id)
|> Map.fetch!(:gas)
# gas from the transaction is needed for pre-Byzantium derived status
Map.put(receipt, "gas", gas)
end
defp responses_to_receipts(responses, id_to_transaction_params)
when is_list(responses) and is_map(id_to_transaction_params) do
Enum.map(responses, &response_to_receipt(&1, id_to_transaction_params))
end
end

@ -56,15 +56,52 @@ defmodule EthereumJSONRPC.Transaction do
index: non_neg_integer(),
input: String.t(),
nonce: non_neg_integer(),
public_key: String.t(),
r: non_neg_integer(),
s: non_neg_integer(),
standard_v: 0 | 1,
to_address_hash: EthereumJSONRPC.address(),
v: non_neg_integer(),
value: non_neg_integer()
}
@doc """
Geth `elixir` can be converted to `params`. Geth does not supply `"publicKey"` or `"standardV"`, unlike Parity.
iex> EthereumJSONRPC.Transaction.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gas" => 21000,
...> "gasPrice" => 50000000000000,
...> "hash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "input" => "0x",
...> "nonce" => 0,
...> "r" => 61965845294689009770156372156374760022787886965323743865986648153755601564112,
...> "s" => 31606574786494953692291101914709926755545765281581808821704454381804773090106,
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionIndex" => 0,
...> "v" => 28,
...> "value" => 31337
...> }
...> )
%{
block_hash: "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
block_number: 46147,
from_address_hash: "0xa1e4380a3b1f749673e270229993ee55f35663b4",
gas: 21000,
gas_price: 50000000000000,
hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
index: 0,
input: "0x",
nonce: 0,
r: 61965845294689009770156372156374760022787886965323743865986648153755601564112,
s: 31606574786494953692291101914709926755545765281581808821704454381804773090106,
to_address_hash: "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
v: 28,
value: 31337
}
"""
@spec elixir_to_params(elixir) :: params
def elixir_to_params(%{
"blockHash" => block_hash,
@ -75,10 +112,8 @@ defmodule EthereumJSONRPC.Transaction do
"hash" => hash,
"input" => input,
"nonce" => nonce,
"publicKey" => public_key,
"r" => r,
"s" => s,
"standardV" => standard_v,
"to" => to_address_hash,
"transactionIndex" => index,
"v" => v,
@ -94,10 +129,8 @@ defmodule EthereumJSONRPC.Transaction do
index: index,
input: input,
nonce: nonce,
public_key: public_key,
r: r,
s: s,
standard_v: standard_v,
to_address_hash: to_address_hash,
v: v,
value: value
@ -117,10 +150,8 @@ defmodule EthereumJSONRPC.Transaction do
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: 0,
...> v: "0x8d",
...> value: 0
...> }

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

@ -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"
],
elixir: "~> 1.6",
elixirc_paths: elixirc_paths(Mix.env()),
lockfile: "../../mix.lock",
preferred_cli_env: [
coveralls: :test,
@ -45,6 +46,10 @@ defmodule EthereumJsonrpc.MixProject do
] ++ env_aliases(env)
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["test/support" | elixirc_paths(:dev)]
defp elixirc_paths(_), do: ["lib"]
defp env_aliases(:dev), do: []
defp env_aliases(_env), do: [compile: "compile --warnings-as-errors"]

@ -1,130 +1,134 @@
defmodule EthereumJSONRPCTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC
import EthereumJSONRPC.Case
describe "fetch_balances/1" do
test "with all valid hash_data returns {:ok, addresses_params}" do
assert EthereumJSONRPC.fetch_balances([
%{block_quantity: "0x1", hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}
]) ==
{:ok,
[
%{
fetched_balance: 1,
fetched_balance_block_number: 1,
hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
}
]}
end
test "with all invalid hash_data returns {:error, reasons}" do
assert EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}]) ==
{:error,
[
%{
"blockNumber" => "0x1",
"code" => -32602,
"hash" => "0x0",
"message" =>
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40."
}
]}
end
@moduletag :capture_log
test "with a mix of valid and invalid hash_data returns {:error, reasons}" do
assert EthereumJSONRPC.fetch_balances([
# start with :ok
%{
block_quantity: "0x1",
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
},
# :ok, :ok clause
%{
block_quantity: "0x34",
hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
},
# :ok, :error clause
%{
block_quantity: "0x2",
hash_data: "0x3"
},
# :error, :ok clause
%{
block_quantity: "0x35",
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
},
# :error, :error clause
%{
block_quantity: "0x4",
hash_data: "0x5"
}
]) ==
{:error,
[
%{
"blockNumber" => "0x2",
"code" => -32602,
"hash" => "0x3",
"message" =>
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40."
},
%{
"blockNumber" => "0x4",
"code" => -32602,
"hash" => "0x5",
"message" =>
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40."
}
]}
end
setup do
%{variant: EthereumJSONRPC.config(:variant)}
end
describe "json_rpc/2" do
# regression test for https://github.com/poanetwork/poa-explorer/issues/254
test "transparently splits batch payloads that would trigger a 413 Request Entity Too Large" do
block_numbers = 0..13000
describe "fetch_balances/1" do
test "with all valid hash_data returns {:ok, addresses_params}", %{variant: variant} do
assert {:ok,
[
%{
fetched_balance: fetched_balance,
fetched_balance_block_number: 1,
hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
}
]} =
EthereumJSONRPC.fetch_balances([
%{block_quantity: "0x1", hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}
])
case variant do
EthereumJSONRPC.Geth ->
assert fetched_balance == 0
EthereumJSONRPC.Parity ->
assert fetched_balance == 1
_ ->
raise ArgumentError, "Unsupported variant (#{variant}})"
end
end
payload =
block_numbers
|> Stream.with_index()
|> Enum.map(&get_block_by_number_request/1)
test "with all invalid hash_data returns {:error, reasons}", %{variant: variant} do
assert {:error, reasons} = EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}])
assert is_list(reasons)
assert length(reasons) == 1
assert_payload_too_large(payload)
[reason] = reasons
url = EthereumJSONRPC.config(:url)
assert %{
"blockNumber" => "0x1",
"code" => -32602,
"hash" => "0x0",
"message" => message
} = reason
assert {:ok, responses} = EthereumJSONRPC.json_rpc(payload, url)
assert Enum.count(responses) == Enum.count(block_numbers)
case variant do
EthereumJSONRPC.Geth ->
assert message ==
"invalid argument 0: json: cannot unmarshal hex string of odd length into Go value of type common.Address"
block_number_set = MapSet.new(block_numbers)
EthereumJSONRPC.Parity ->
assert message ==
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40."
response_block_number_set =
Enum.into(responses, MapSet.new(), fn %{"result" => %{"number" => quantity}} ->
EthereumJSONRPC.quantity_to_integer(quantity)
end)
_ ->
raise ArgumentError, "Unsupported variant (#{variant}})"
end
end
assert MapSet.equal?(response_block_number_set, block_number_set)
test "with a mix of valid and invalid hash_data returns {:error, reasons}" do
assert {:error, reasons} =
EthereumJSONRPC.fetch_balances([
# start with :ok
%{
block_quantity: "0x1",
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
},
# :ok, :ok clause
%{
block_quantity: "0x34",
hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
},
# :ok, :error clause
%{
block_quantity: "0x2",
hash_data: "0x3"
},
# :error, :ok clause
%{
block_quantity: "0x35",
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
},
# :error, :error clause
%{
block_quantity: "0x4",
hash_data: "0x5"
}
])
assert is_list(reasons)
assert length(reasons) > 1
end
end
defp assert_payload_too_large(payload) do
json = Jason.encode_to_iodata!(payload)
headers = [{"Content-Type", "application/json"}]
url = EthereumJSONRPC.config(:url)
assert {:ok, %HTTPoison.Response{body: body, status_code: 413}} =
HTTPoison.post(url, json, headers, EthereumJSONRPC.config(:http))
describe "fetch_block_number_by_tag/1" do
@tag capture_log: false
test "with earliest" do
log_bad_gateway(
fn -> EthereumJSONRPC.fetch_block_number_by_tag("earliest") end,
fn result ->
assert {:ok, 0} = result
end
)
end
assert body =~ "413 Request Entity Too Large"
end
@tag capture_log: false
test "with latest" do
log_bad_gateway(
fn -> EthereumJSONRPC.fetch_block_number_by_tag("latest") end,
fn result ->
assert {:ok, number} = result
assert number > 0
end
)
end
defp get_block_by_number_request({block_number, id}) do
%{
"id" => id,
"jsonrpc" => "2.0",
"method" => "eth_getBlockByNumber",
"params" => [EthereumJSONRPC.integer_to_quantity(block_number), true]
}
@tag capture_log: false
test "with pending" do
log_bad_gateway(
fn -> EthereumJSONRPC.fetch_block_number_by_tag("pending") end,
fn result ->
assert {:ok, number} = result
assert number > 0
end
)
end
end
end

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

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

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

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

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

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

@ -1,4 +1,4 @@
defmodule Explorer.Chain.Hash.Truncated do
defmodule Explorer.Chain.Hash.Address do
@moduledoc """
The address (40 (hex) characters / 160 bits / 20 bytes) is derived from the public key (128 (hex) characters /
512 bits / 64 bytes) which is derived from the private key (64 (hex) characters / 256 bits / 32 bytes).
@ -24,7 +24,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the `term` is already in `t:t/0`, then it is returned
iex> Explorer.Chain.Hash.Truncated.cast(
iex> Explorer.Chain.Hash.Address.cast(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
@ -40,7 +40,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the `term` is an `non_neg_integer`, then it is converted to `t:t/0`
iex> Explorer.Chain.Hash.Truncated.cast(0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed)
iex> Explorer.Chain.Hash.Address.cast(0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed)
{
:ok,
%Explorer.Chain.Hash{
@ -51,12 +51,12 @@ defmodule Explorer.Chain.Hash.Truncated do
If the `non_neg_integer` is too large, then `:error` is returned.
iex> Explorer.Chain.Hash.Truncated.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b)
iex> Explorer.Chain.Hash.Address.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b)
:error
If the `term` is a `String.t` that starts with `0x`, then is converted to an integer and then to `t:t/0`.
iex> Explorer.Chain.Hash.Truncated.cast("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
iex> Explorer.Chain.Hash.Address.cast("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
{
:ok,
%Explorer.Chain.Hash{
@ -68,7 +68,7 @@ defmodule Explorer.Chain.Hash.Truncated do
While `non_neg_integers` don't have to be the correct width (because zero padding it difficult with numbers),
`String.t` format must always have #{@hexadecimal_digit_count} digits after the `0x` base prefix.
iex> Explorer.Chain.Hash.Truncated.cast("0x0")
iex> Explorer.Chain.Hash.Address.cast("0x0")
:error
"""
@ -83,7 +83,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the field from the struct is `t:t/0`, then it succeeds
iex> Explorer.Chain.Hash.Truncated.dump(
iex> Explorer.Chain.Hash.Address.dump(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
@ -93,7 +93,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the field from the struct is an incorrect format such as `t:Explorer.Chain.Hash.t/0`, `:error` is returned
iex> Explorer.Chain.Hash.Truncated.dump(
iex> Explorer.Chain.Hash.Address.dump(
...> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
@ -114,7 +114,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the binary hash is the correct format, it is returned.
iex> Explorer.Chain.Hash.Truncated.load(
iex> Explorer.Chain.Hash.Address.load(
...> <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
...> )
{
@ -127,7 +127,7 @@ defmodule Explorer.Chain.Hash.Truncated do
If the binary hash is an incorrect format, such as if an `Explorer.Chain.Hash` field is loaded, `:error` is returned.
iex> Explorer.Chain.Hash.Truncated.load(
iex> Explorer.Chain.Hash.Address.load(
...> <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
...> )
:error

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

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

@ -32,7 +32,7 @@ defmodule Explorer.Chain.SmartContract do
Address,
foreign_key: :address_hash,
references: :hash,
type: Hash.Truncated
type: Hash.Address
)
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
internal_transactions_indexed_at status to_address_hash)a
@required_attrs ~w(gas gas_price hash input nonce public_key r s standard_v v value)a
@typedoc """
The full public key of the signer of the transaction.
"""
@type public_key :: Data.t()
@required_attrs ~w(gas gas_price hash input nonce r s v value)a
@typedoc """
X coordinate module n in
@ -30,28 +25,6 @@ defmodule Explorer.Chain.Transaction do
"""
@type s :: Decimal.t()
@typedoc """
For message signatures, we use a trick called public key recovery. The fact is that if you have the full R point
(not just its X coordinate) and `t:s/0`, and a message, you can compute for which public key this would be a valid
signature. What this allows is to 'verify' a message with an address, without needing to know the full key (we just to
public key recovery on the signature, and then hash the recovered key and compare it with the address).
However, this means we need the full R coordinates. There can be up to 4 different points with a given
"X coordinate modulo n". (2 because each X coordinate has two possible Y coordinates, and 2 because r+n may still be a
valid X coordinate). That number between 0 and 3 is standard_v.
| `standard_v` | X | Y |
|---------------|--------|------|
| `0` | lower | even |
| `1` | lower | odd |
| `2` | higher | even |
| `3` | higher | odd |
**Note: that `2` and `3` are exceedingly rarely, and will in practice only ever be seen in specifically generated
examples.**
"""
@type standard_v :: 0..3
@typedoc """
The index of the transaction in its block.
"""
@ -96,12 +69,10 @@ defmodule Explorer.Chain.Transaction do
* `internal_transactions_indexed_at` - when `internal_transactions` were fetched by `Explorer.Indexer`.
* `logs` - events that occurred while mining the `transaction`.
* `nonce` - the number of transaction made by the sender prior to this one
* `public_key` - public key of the signer of the transaction
* `r` - the R field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as
the X coordinate of a point R, modulo the curve order n.
* `s` - The S field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as
the X coordinate of a point R, modulo the curve order n.
* `standard_v` - The standardized V field of the signature
* `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending.
* `to_address` - sink of `value`
* `to_address_hash` - `to_address` foreign key
@ -114,7 +85,7 @@ defmodule Explorer.Chain.Transaction do
block_number: Block.block_number() | nil,
cumulative_gas_used: Gas.t() | nil,
from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_hash: Hash.Truncated.t(),
from_address_hash: Hash.Address.t(),
gas: Gas.t(),
gas_price: wei_per_gas,
gas_used: Gas.t() | nil,
@ -125,13 +96,11 @@ defmodule Explorer.Chain.Transaction do
internal_transactions_indexed_at: DateTime.t(),
logs: %Ecto.Association.NotLoaded{} | [Log.t()],
nonce: non_neg_integer(),
public_key: public_key(),
r: r(),
s: s(),
standard_v: standard_v(),
status: Status.t() | nil,
to_address: %Ecto.Association.NotLoaded{} | Address.t(),
to_address_hash: Hash.Truncated.t(),
to_address_hash: Hash.Address.t(),
v: v(),
value: Wei.t()
}
@ -147,14 +116,12 @@ defmodule Explorer.Chain.Transaction do
field(:internal_transactions_indexed_at, :utc_datetime)
field(:input, Data)
field(:nonce, :integer)
field(:public_key, Data)
field(:r, :decimal)
field(:s, :decimal)
field(:standard_v, :integer)
field(:status, Status)
field(:v, :integer)
field(:value, Wei)
field(:created_contract_address_hash, Hash.Truncated, virtual: true)
field(:created_contract_address_hash, Hash.Address, virtual: true)
timestamps()
@ -165,7 +132,7 @@ defmodule Explorer.Chain.Transaction do
Address,
foreign_key: :from_address_hash,
references: :hash,
type: Hash.Truncated
type: Hash.Address
)
has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash)
@ -176,7 +143,7 @@ defmodule Explorer.Chain.Transaction do
Address,
foreign_key: :to_address_hash,
references: :hash,
type: Hash.Truncated
type: Hash.Address
)
end
@ -191,10 +158,8 @@ defmodule Explorer.Chain.Transaction do
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> v: 0x8d,
...> value: 0
...> }
@ -217,10 +182,8 @@ defmodule Explorer.Chain.Transaction do
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> status: :ok,
...> v: 0x8d,
...> value: 0
@ -254,10 +217,8 @@ defmodule Explorer.Chain.Transaction do
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> status: :ok,
...> v: 0x8d,
...> value: 0
@ -278,10 +239,8 @@ defmodule Explorer.Chain.Transaction do
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: 0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75,
...> s: 0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3,
...> standard_v: 0x0,
...> v: 0x8d,
...> value: 0
...> }
@ -305,7 +264,6 @@ defmodule Explorer.Chain.Transaction do
|> cast(attrs, @required_attrs ++ @optional_attrs)
|> validate_required(@required_attrs)
|> validate_collated_or_pending()
|> validate_number(:standard_v, greater_than_or_equal_to: 0, less_than_or_equal_to: 3)
|> check_pending()
|> check_collated()
|> foreign_key_constraint(:block_hash)

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

@ -23,10 +23,8 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do
add(:internal_transactions_indexed_at, :utc_datetime, null: true)
add(:nonce, :integer, null: false)
add(:public_key, :bytea, null: false)
add(:r, :numeric, precision: 100, null: false)
add(:s, :numeric, precision: 100, null: false)
add(:standard_v, :smallint, null: false)
# `null` when a pending transaction
add(:status, :integer, null: true)
@ -128,8 +126,6 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do
)
)
create(constraint(:transactions, :standard_v, check: "0 <= standard_v AND standard_v <= 3"))
create(index(:transactions, :block_hash))
create(index(:transactions, :from_address_hash))
create(index(:transactions, :to_address_hash))

@ -5,7 +5,10 @@ defmodule Explorer.Repo.Migrations.CreateLogs do
create table(:logs) do
add(:data, :bytea, null: false)
add(:index, :integer, null: false)
add(:type, :string, null: false)
# Parity supplies it; Geth does not.
add(:type, :string, null: true)
add(:first_topic, :string, null: true)
add(:second_topic, :string, null: true)
add(:third_topic, :string, null: true)

@ -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,
input: "0x5c8eff12",
nonce: "31337",
public_key: "0xb39af9cb",
r: 0x9,
s: 0x10,
standard_v: 0x1,
transaction_index: "0x12",
v: 27
})

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

@ -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 """
Asynchronously fetches balances for each address `hash` at the `block_number`.
"""
@spec async_fetch_balances([%{required(:block_number) => Block.block_number(), required(:hash) => Hash.Truncated.t()}]) ::
@spec async_fetch_balances([%{required(:block_number) => Block.block_number(), required(:hash) => Hash.Address.t()}]) ::
:ok
def async_fetch_balances(address_fields) when is_list(address_fields) do
params_list = Enum.map(address_fields, &address_fields_to_params/1)

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

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

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

@ -6,23 +6,37 @@ defmodule Indexer.AddressBalanceFetcherTest do
alias Explorer.Chain.{Address, Hash, Wei}
alias Indexer.{AddressBalanceFetcher, AddressBalanceFetcherCase}
@block_number 2_932_838
@hash %Explorer.Chain.Hash{
byte_count: 20,
bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>>
}
setup do
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
:ok
%{variant: EthereumJSONRPC.config(:variant)}
end
describe "init/1" do
test "fetches unfetched Block miner balance" do
{:ok, miner_hash} = Hash.Truncated.cast("0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca")
test "fetches unfetched Block miner balance", %{variant: variant} do
%{block_number: block_number, fetched_balance: fetched_balance, miner_hash_data: miner_hash_data} =
case variant do
EthereumJSONRPC.Geth ->
%{
block_number: 201_480,
fetched_balance: 6_301_752_965_671_077_173,
miner_hash_data: "0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca"
}
EthereumJSONRPC.Parity ->
%{
block_number: 34,
fetched_balance: 252_460_834_000_000_000_000_000_000,
miner_hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
}
_ ->
raise ArgumentError, "Unsupported variant (#{variant})"
end
{:ok, miner_hash} = Hash.Address.cast(miner_hash_data)
miner = insert(:address, hash: miner_hash)
block = insert(:block, miner: miner, number: 34)
block = insert(:block, miner: miner, number: block_number)
assert miner.fetched_balance == nil
assert miner.fetched_balance_block_number == nil
@ -36,14 +50,34 @@ defmodule Indexer.AddressBalanceFetcherTest do
)
end)
assert fetched_address.fetched_balance == %Wei{value: Decimal.new(252_460_834_000_000_000_000_000_000)}
assert fetched_address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)}
assert fetched_address.fetched_balance_block_number == block.number
end
test "fetches unfetched addresses when less than max batch size" do
{:ok, miner_hash} = Hash.Truncated.cast("0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca")
test "fetches unfetched addresses when less than max batch size", %{variant: variant} do
%{block_number: block_number, fetched_balance: fetched_balance, miner_hash_data: miner_hash_data} =
case variant do
EthereumJSONRPC.Geth ->
%{
block_number: 201_480,
fetched_balance: 6_301_752_965_671_077_173,
miner_hash_data: "0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca"
}
EthereumJSONRPC.Parity ->
%{
block_number: 34,
fetched_balance: 252_460_834_000_000_000_000_000_000,
miner_hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
}
_ ->
raise ArgumentError, "Unsupported variant (#{variant})"
end
{:ok, miner_hash} = Hash.Address.cast(miner_hash_data)
miner = insert(:address, hash: miner_hash)
block = insert(:block, miner: miner, number: 34)
block = insert(:block, miner: miner, number: block_number)
AddressBalanceFetcherCase.start_supervised!(max_batch_size: 2)
@ -54,43 +88,93 @@ defmodule Indexer.AddressBalanceFetcherTest do
)
end)
assert fetched_address.fetched_balance == %Wei{value: Decimal.new(252_460_834_000_000_000_000_000_000)}
assert fetched_address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)}
assert fetched_address.fetched_balance_block_number == block.number
end
end
describe "async_fetch_balances/1" do
test "fetches balances for address_hashes" do
test "fetches balances for address_hashes", %{variant: variant} do
AddressBalanceFetcherCase.start_supervised!()
assert :ok = AddressBalanceFetcher.async_fetch_balances([%{block_number: @block_number, hash: @hash}])
%{block_number: block_number, fetched_balance: fetched_balance, hash: hash} =
case variant do
EthereumJSONRPC.Geth ->
%{
block_number: 201_480,
fetched_balance: 6_301_752_965_671_077_173,
hash: %Explorer.Chain.Hash{
byte_count: 20,
bytes: <<230, 167, 161, 212, 127, 242, 27, 99, 33, 22, 42, 234, 124, 108, 180, 87, 213, 71, 107, 202>>
}
}
EthereumJSONRPC.Parity ->
%{
block_number: 34,
fetched_balance: 252_460_834_000_000_000_000_000_000,
hash: %Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122, 202>>
}
}
_ ->
raise ArgumentError, "Unsupported variant (#{variant})"
end
assert :ok = AddressBalanceFetcher.async_fetch_balances([%{block_number: block_number, hash: hash}])
address =
wait(fn ->
Repo.get!(Address, @hash)
Repo.get!(Address, hash)
end)
assert address.fetched_balance == %Wei{value: Decimal.new(1)}
assert address.fetched_balance_block_number == @block_number
assert address.fetched_balance == %Wei{value: Decimal.new(fetched_balance)}
assert address.fetched_balance_block_number == block_number
end
end
describe "run/2" do
test "duplicate address hashes the max block_quantity" do
hash_data = "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
assert AddressBalanceFetcher.run(
[%{block_quantity: "0x1", hash_data: hash_data}, %{block_quantity: "0x2", hash_data: hash_data}],
0
) == :ok
fetched_address = Repo.one!(from(address in Address, where: address.hash == ^hash_data))
assert fetched_address.fetched_balance == %Explorer.Chain.Wei{
value: Decimal.new(252_460_802_000_000_000_000_000_000)
}
assert fetched_address.fetched_balance_block_number == 2
@tag capture_log: true
test "duplicate address hashes the max block_quantity", %{variant: variant} do
%{fetched_balance: fetched_balance, hash_data: hash_data} =
case variant do
EthereumJSONRPC.Geth ->
%{
fetched_balance: 5_000_000_000_000_000_000,
hash_data: "0x05a56e2d52c817161883f50c441c3228cfe54d9f"
}
EthereumJSONRPC.Parity ->
%{
fetched_balance: 252_460_802_000_000_000_000_000_000,
hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
}
_ ->
raise ArgumentError, "Unsupported variant (#{variant})"
end
case AddressBalanceFetcher.run(
[%{block_quantity: "0x1", hash_data: hash_data}, %{block_quantity: "0x2", hash_data: hash_data}],
0
) do
:ok ->
fetched_address = Repo.one!(from(address in Address, where: address.hash == ^hash_data))
assert fetched_address.fetched_balance == %Explorer.Chain.Wei{
value: Decimal.new(fetched_balance)
}
assert fetched_address.fetched_balance_block_number == 2
other ->
# not all nodes behind the `https://mainnet.infura.io` pool are fully-synced. Node that aren't fully-synced
# won't have historical address balances.
assert {:retry, [%{block_quantity: "0x2", hash_data: ^hash_data}]} = other
end
end
test "duplicate address hashes only retry max block_quantity" do

@ -3,6 +3,7 @@ defmodule Indexer.BlockFetcherTest do
use Explorer.DataCase, async: false
import ExUnit.CaptureLog
import EthereumJSONRPC.Case
alias Explorer.Chain.{Address, Block, Log, Transaction, Wei}
@ -36,6 +37,10 @@ defmodule Indexer.BlockFetcherTest do
# ON blocks.hash = transactions.block_hash) as blocks
@first_full_block_number 37
setup do
%{variant: EthereumJSONRPC.config(:variant)}
end
describe "start_link/1" do
test "starts fetching blocks from latest and goes down" do
{:ok, latest_block_number} = EthereumJSONRPC.fetch_block_number_by_tag("latest")
@ -137,104 +142,231 @@ defmodule Indexer.BlockFetcherTest do
%{state: state}
end
test "with single element range that is valid imports one block", %{state: state} do
test "with single element range that is valid imports one block", %{state: state, variant: variant} do
{:ok, sequence} = Sequence.start_link([], 0, 1)
assert {:ok,
%{
addresses: [
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
} = address_hash
],
blocks: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<91, 40, 193, 191, 211, 161, 82, 48, 201, 164, 107, 57, 156, 208, 249, 166, 146, 13, 67, 46, 133,
56, 28, 198, 161, 64, 176, 110, 132, 16, 17, 47>>
}
],
logs: [],
transactions: []
}} = BlockFetcher.import_range(0..0, state, sequence)
%{address_hash: address_hash, block_hash: block_hash} =
case variant do
EthereumJSONRPC.Geth ->
%{
address_hash: %Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
},
block_hash: %Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<212, 229, 103, 64, 248, 118, 174, 248, 192, 16, 184, 106, 64, 213, 245, 103, 69, 161, 24, 208, 144,
106, 52, 230, 154, 236, 140, 13, 177, 203, 143, 163>>
}
}
EthereumJSONRPC.Parity ->
%{
address_hash: %Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
},
block_hash: %Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<91, 40, 193, 191, 211, 161, 82, 48, 201, 164, 107, 57, 156, 208, 249, 166, 146, 13, 67, 46, 133, 56,
28, 198, 161, 64, 176, 110, 132, 16, 17, 47>>
}
}
_ ->
raise ArgumenrError, "Unsupported variant (#{variant})"
end
log_bad_gateway(
fn -> BlockFetcher.import_range(0..0, state, sequence) end,
fn result ->
assert {:ok,
%{
addresses: [^address_hash],
blocks: [^block_hash],
logs: [],
transactions: []
}} = result
wait_for_tasks(InternalTransactionFetcher)
wait_for_tasks(AddressBalanceFetcher)
wait_for_tasks(InternalTransactionFetcher)
wait_for_tasks(AddressBalanceFetcher)
assert Repo.aggregate(Block, :count, :hash) == 1
assert Repo.aggregate(Address, :count, :hash) == 1
assert Repo.aggregate(Block, :count, :hash) == 1
assert Repo.aggregate(Address, :count, :hash) == 1
address = Repo.get!(Address, address_hash)
address = Repo.get!(Address, address_hash)
assert address.fetched_balance == %Wei{value: Decimal.new(0)}
assert address.fetched_balance_block_number == 0
assert address.fetched_balance == %Wei{value: Decimal.new(0)}
assert address.fetched_balance_block_number == 0
end
)
end
test "can import range with all synchronous imported schemas", %{state: state} do
test "can import range with all synchronous imported schemas", %{state: state, variant: variant} do
{:ok, sequence} = Sequence.start_link([], 0, 1)
assert {:ok,
%{
addresses: [
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>>
} = first_address_hash,
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122, 202>>
} = second_address_hash
],
blocks: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<246, 180, 184, 200, 141, 243, 235, 210, 82, 236, 71, 99, 40, 51, 77, 192, 38, 207, 102, 96, 106,
132, 251, 118, 155, 61, 60, 188, 204, 132, 113, 189>>
}
],
logs: [
%{
index: 0,
transaction_hash: %Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57,
101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>>
}
}
],
transactions: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57,
101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>>
}
]
}} = BlockFetcher.import_range(@first_full_block_number..@first_full_block_number, state, sequence)
case variant do
EthereumJSONRPC.Geth ->
block_number = 48230
wait_for_tasks(InternalTransactionFetcher)
wait_for_tasks(AddressBalanceFetcher)
assert Repo.aggregate(Block, :count, :hash) == 1
assert Repo.aggregate(Address, :count, :hash) == 2
assert Repo.aggregate(Log, :count, :id) == 1
assert Repo.aggregate(Transaction, :count, :hash) == 1
first_address = Repo.get!(Address, first_address_hash)
assert first_address.fetched_balance == %Wei{value: Decimal.new(1)}
assert first_address.fetched_balance_block_number == @first_full_block_number
second_address = Repo.get!(Address, second_address_hash)
assert second_address.fetched_balance == %Wei{value: Decimal.new(252_460_837_000_000_000_000_000_000)}
assert second_address.fetched_balance_block_number == @first_full_block_number
assert {:ok,
%{
addresses: [
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<55, 52, 203, 24, 116, 145, 237, 231, 19, 174, 91, 59, 45, 18, 40, 74, 244, 107, 129, 1>>
} = first_address_hash,
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<89, 47, 120, 202, 98, 102, 132, 20, 109, 56, 18, 133, 202, 0, 221, 145, 179, 117, 253, 17>>
} = second_address_hash,
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<187, 123, 130, 135, 243, 240, 169, 51, 71, 74, 121, 234, 228, 44, 188, 169, 119, 121, 17,
113>>
} = third_address_hash,
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<210, 193, 91, 230, 52, 135, 86, 246, 145, 187, 152, 246, 13, 254, 190, 97, 230, 190, 59,
86>>
} = fourth_address_hash,
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<221, 47, 30, 110, 73, 130, 2, 232, 109, 143, 84, 66, 175, 89, 101, 128, 164, 240, 60, 44>>
} = fifth_address_hash
],
blocks: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<209, 52, 30, 145, 228, 166, 153, 192, 47, 187, 24, 4, 84, 20, 80, 18, 144, 134, 68, 198,
200, 119, 77, 16, 251, 182, 96, 253, 27, 146, 104, 176>>
}
],
logs: [],
transactions: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<76, 188, 236, 37, 153, 153, 224, 115, 252, 79, 176, 224, 228, 166, 18, 66, 94, 61, 115, 57,
47, 162, 37, 255, 36, 96, 161, 238, 171, 66, 99, 10>>
},
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<240, 237, 34, 44, 16, 174, 248, 135, 4, 196, 15, 198, 34, 220, 218, 174, 13, 208, 242, 122,
154, 143, 4, 28, 171, 95, 190, 255, 254, 174, 75, 182>>
}
]
}} = BlockFetcher.import_range(block_number..block_number, state, sequence)
wait_for_tasks(InternalTransactionFetcher)
wait_for_tasks(AddressBalanceFetcher)
assert Repo.aggregate(Block, :count, :hash) == 1
assert Repo.aggregate(Address, :count, :hash) == 5
assert Repo.aggregate(Log, :count, :id) == 0
assert Repo.aggregate(Transaction, :count, :hash) == 2
first_address = Repo.get!(Address, first_address_hash)
assert first_address.fetched_balance == %Wei{value: Decimal.new(1_999_953_415_287_753_599_000)}
assert first_address.fetched_balance_block_number == block_number
second_address = Repo.get!(Address, second_address_hash)
assert second_address.fetched_balance == %Wei{value: Decimal.new(50_000_000_000_000_000)}
assert second_address.fetched_balance_block_number == block_number
third_address = Repo.get!(Address, third_address_hash)
assert third_address.fetched_balance == %Wei{value: Decimal.new(30_827_986_037_499_360_709_544)}
assert third_address.fetched_balance_block_number == block_number
fourth_address = Repo.get!(Address, fourth_address_hash)
assert fourth_address.fetched_balance == %Wei{value: Decimal.new(500_000_000_001_437_727_304)}
assert fourth_address.fetched_balance_block_number == block_number
fifth_address = Repo.get!(Address, fifth_address_hash)
assert fifth_address.fetched_balance == %Wei{value: Decimal.new(930_417_572_224_879_702_000)}
assert fifth_address.fetched_balance_block_number == block_number
EthereumJSONRPC.Parity ->
assert {:ok,
%{
addresses: [
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65,
91>>
} = first_address_hash,
%Explorer.Chain.Hash{
byte_count: 20,
bytes:
<<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122,
202>>
} = second_address_hash
],
blocks: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<246, 180, 184, 200, 141, 243, 235, 210, 82, 236, 71, 99, 40, 51, 77, 192, 38, 207, 102, 96,
106, 132, 251, 118, 155, 61, 60, 188, 204, 132, 113, 189>>
}
],
logs: [
%{
index: 0,
transaction_hash: %Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77,
57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>>
}
}
],
transactions: [
%Explorer.Chain.Hash{
byte_count: 32,
bytes:
<<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77,
57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>>
}
]
}} = BlockFetcher.import_range(@first_full_block_number..@first_full_block_number, state, sequence)
wait_for_tasks(InternalTransactionFetcher)
wait_for_tasks(AddressBalanceFetcher)
assert Repo.aggregate(Block, :count, :hash) == 1
assert Repo.aggregate(Address, :count, :hash) == 2
assert Repo.aggregate(Log, :count, :id) == 1
assert Repo.aggregate(Transaction, :count, :hash) == 1
first_address = Repo.get!(Address, first_address_hash)
assert first_address.fetched_balance == %Wei{value: Decimal.new(1)}
assert first_address.fetched_balance_block_number == @first_full_block_number
second_address = Repo.get!(Address, second_address_hash)
assert second_address.fetched_balance == %Wei{value: Decimal.new(252_460_837_000_000_000_000_000_000)}
assert second_address.fetched_balance_block_number == @first_full_block_number
_ ->
raise ArgumentError, "Unsupport variant (#{variant})"
end
end
end
@ -292,7 +424,7 @@ defmodule Indexer.BlockFetcherTest do
end
defp wait_for_tasks(buffered_task) do
wait_until(5000, fn ->
wait_until(10_000, fn ->
counts = BufferedTask.debug_count(buffered_task)
counts.buffer == 0 and counts.tasks == 0
end)

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

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

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

Loading…
Cancel
Save