Land #162: Optimized indexer

pull/170/head
Luke Imhoff 7 years ago committed by GitHub
commit d4234d3246
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .circleci/config.yml
  2. 37
      .credo.exs
  3. 2
      .formatter.exs
  4. 4
      apps/ethereum_jsonrpc/.formatter.exs
  5. 24
      apps/ethereum_jsonrpc/.gitignore
  6. 36
      apps/ethereum_jsonrpc/README.md
  7. 8
      apps/ethereum_jsonrpc/config/config.exs
  8. 303
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  9. 16
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/application.ex
  10. 299
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
  11. 214
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex
  12. 119
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex
  13. 21
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/logs.ex
  14. 69
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex
  15. 432
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace.ex
  16. 52
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace/action.ex
  17. 42
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace/result.ex
  18. 17
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/traces.ex
  19. 158
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex
  20. 216
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex
  21. 155
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex
  22. 154
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex
  23. 65
      apps/ethereum_jsonrpc/mix.exs
  24. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs
  25. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/blocks_test.exs
  26. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/log_test.exs
  27. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace/action_test.exs
  28. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace/result_test.exs
  29. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace_test.exs
  30. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs
  31. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipt_test.exs
  32. 43
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs
  33. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/transaction_test.exs
  34. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/transactions_test.exs
  35. 7
      apps/ethereum_jsonrpc/test/test_helper.exs
  36. 25
      apps/explorer/config/config.exs
  37. 25
      apps/explorer/config/dev.exs
  38. 4
      apps/explorer/config/dev.secret.exs.example
  39. 41
      apps/explorer/config/prod.exs
  40. 5
      apps/explorer/config/test.exs
  41. 14
      apps/explorer/lib/backfill_transaction_receipt_ids.ex
  42. 13
      apps/explorer/lib/explorer/application.ex
  43. 1574
      apps/explorer/lib/explorer/chain.ex
  44. 78
      apps/explorer/lib/explorer/chain/address.ex
  45. 68
      apps/explorer/lib/explorer/chain/block.ex
  46. 25
      apps/explorer/lib/explorer/chain/block_transaction.ex
  47. 18
      apps/explorer/lib/explorer/chain/credit.ex
  48. 17
      apps/explorer/lib/explorer/chain/debit.ex
  49. 21
      apps/explorer/lib/explorer/chain/from_address.ex
  50. 220
      apps/explorer/lib/explorer/chain/hash.ex
  51. 150
      apps/explorer/lib/explorer/chain/hash/full.ex
  52. 151
      apps/explorer/lib/explorer/chain/hash/truncated.ex
  53. 504
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  54. 116
      apps/explorer/lib/explorer/chain/internal_transaction/call_type.ex
  55. 114
      apps/explorer/lib/explorer/chain/internal_transaction/type.ex
  56. 104
      apps/explorer/lib/explorer/chain/log.ex
  57. 50
      apps/explorer/lib/explorer/chain/receipt.ex
  58. 109
      apps/explorer/lib/explorer/chain/receipt/status.ex
  59. 76
      apps/explorer/lib/explorer/chain/statistics.ex
  60. 20
      apps/explorer/lib/explorer/chain/statistics/server.ex
  61. 20
      apps/explorer/lib/explorer/chain/to_address.ex
  62. 287
      apps/explorer/lib/explorer/chain/transaction.ex
  63. 8
      apps/explorer/lib/explorer/chain/wei.ex
  64. 19
      apps/explorer/lib/explorer/ethereum/ethereum.ex
  65. 14
      apps/explorer/lib/explorer/ethereum/live.ex
  66. 9
      apps/explorer/lib/explorer/ethereum/test.ex
  67. 14
      apps/explorer/lib/explorer/ethereumex_extensions.ex
  68. 10
      apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex
  69. 18
      apps/explorer/lib/explorer/exchange_rates/token.ex
  70. 5
      apps/explorer/lib/explorer/exq_node_identifier.ex
  71. 17
      apps/explorer/lib/explorer/importers/balance_importer.ex
  72. 81
      apps/explorer/lib/explorer/importers/block_importer.ex
  73. 80
      apps/explorer/lib/explorer/importers/internal_transaction_importer.ex
  74. 79
      apps/explorer/lib/explorer/importers/receipt_importer.ex
  75. 142
      apps/explorer/lib/explorer/importers/transaction_importer.ex
  76. 55
      apps/explorer/lib/explorer/indexer.ex
  77. 157
      apps/explorer/lib/explorer/indexer/address_fetcher.ex
  78. 305
      apps/explorer/lib/explorer/indexer/block_fetcher.ex
  79. 80
      apps/explorer/lib/explorer/indexer/sequence.ex
  80. 24
      apps/explorer/lib/explorer/indexer/supervisor.ex
  81. 4
      apps/explorer/lib/explorer/market/history/cataloger.ex
  82. 2
      apps/explorer/lib/explorer/market/market.ex
  83. 6
      apps/explorer/lib/explorer/market/market_history.ex
  84. 24
      apps/explorer/lib/explorer/repo.ex
  85. 4
      apps/explorer/lib/explorer/scheduler.ex
  86. 21
      apps/explorer/lib/explorer/skipped_balances.ex
  87. 32
      apps/explorer/lib/explorer/skipped_blocks.ex
  88. 25
      apps/explorer/lib/explorer/skipped_internal_transactions.ex
  89. 25
      apps/explorer/lib/explorer/skipped_receipts.ex
  90. 13
      apps/explorer/lib/explorer/workers/import_balance.ex
  91. 26
      apps/explorer/lib/explorer/workers/import_block.ex
  92. 12
      apps/explorer/lib/explorer/workers/import_internal_transaction.ex
  93. 12
      apps/explorer/lib/explorer/workers/import_receipt.ex
  94. 18
      apps/explorer/lib/explorer/workers/import_skipped_blocks.ex
  95. 26
      apps/explorer/lib/explorer/workers/import_transaction.ex
  96. 29
      apps/explorer/lib/explorer/workers/refresh_balance.ex
  97. 45
      apps/explorer/lib/giant_address_migrator.ex
  98. 25
      apps/explorer/lib/mix/tasks/exq.start.ex
  99. 24
      apps/explorer/lib/mix/tasks/scrape.balances.ex
  100. 26
      apps/explorer/lib/mix/tasks/scrape.blocks.ex
  101. Some files were not shown because too many files have changed in this diff Show More

@ -229,7 +229,7 @@ jobs:
paths: paths:
- plts - plts
- run: mix dialyzer - run: mix dialyzer --halt-exit-status
eslint: eslint:
docker: docker:
# Ensure .tool-versions matches # Ensure .tool-versions matches
@ -314,7 +314,6 @@ jobs:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
# match PGUSER for elixir image above # match PGUSER for elixir image above
POSTGRES_USER: postgres POSTGRES_USER: postgres
- image: circleci/redis:4.0.9-alpine
working_directory: ~/app working_directory: ~/app
@ -329,10 +328,6 @@ jobs:
name: Wait for DB name: Wait for DB
command: dockerize -wait tcp://localhost:5432 -timeout 1m command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run:
name: Wait for Redis
command: dockerize -wait tcp://localhost:6379 -timeout 1m
- run: mix coveralls.circle --umbrella - run: mix coveralls.circle --umbrella
- store_test_results: - store_test_results:

@ -53,17 +53,29 @@
# {Credo.Check.Design.DuplicatedCode, false} # {Credo.Check.Design.DuplicatedCode, false}
# #
checks: [ checks: [
# outdated by formatter in Elixir 1.6. See https://github.com/rrrene/credo/issues/505
{Credo.Check.Consistency.LineEndings, false},
{Credo.Check.Consistency.SpaceAroundOperators, false},
{Credo.Check.Consistency.SpaceInParentheses, false},
{Credo.Check.Consistency.TabsOrSpaces, false},
{Credo.Check.Readability.LargeNumbers, false},
{Credo.Check.Readability.MaxLineLength, false},
{Credo.Check.Readability.ParenthesesInCondition, false},
{Credo.Check.Readability.RedundantBlankLines, false},
{Credo.Check.Readability.Semicolons, false},
{Credo.Check.Readability.SpaceAfterCommas, false},
{Credo.Check.Readability.TrailingBlankLine, false},
{Credo.Check.Readability.TrailingWhiteSpace, false},
# not handled by formatter
{Credo.Check.Consistency.ExceptionNames}, {Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.LineEndings},
{Credo.Check.Consistency.ParameterPatternMatching}, {Credo.Check.Consistency.ParameterPatternMatching},
{Credo.Check.Consistency.SpaceAroundOperators},
{Credo.Check.Consistency.SpaceInParentheses},
{Credo.Check.Consistency.TabsOrSpaces},
# You can customize the priority of any check # You can customize the priority of any check
# Priority values are: `low, normal, high, higher` # Priority values are: `low, normal, high, higher`
# #
{Credo.Check.Design.AliasUsage, excluded_lastnames: ~w(Number Time), priority: :low}, {Credo.Check.Design.AliasUsage,
excluded_lastnames: ~w(DateTime Full Number Repo Time Truncated), priority: :low},
# For some checks, you can also set other parameters # For some checks, you can also set other parameters
# #
@ -77,25 +89,17 @@
# If you don't want TODO comments to cause `mix credo` to fail, just # If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero). # set this value to 0 (zero).
# #
{Credo.Check.Design.TagTODO, exit_status: 2}, {Credo.Check.Design.TagTODO, exit_status: 0},
{Credo.Check.Design.TagFIXME}, {Credo.Check.Design.TagFIXME},
{Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 120},
{Credo.Check.Readability.ModuleAttributeNames}, {Credo.Check.Readability.ModuleAttributeNames},
{Credo.Check.Readability.ModuleDoc}, {Credo.Check.Readability.ModuleDoc},
{Credo.Check.Readability.ModuleNames}, {Credo.Check.Readability.ModuleNames},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs},
{Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PredicateFunctionNames}, {Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.PreferImplicitTry}, {Credo.Check.Readability.PreferImplicitTry},
{Credo.Check.Readability.RedundantBlankLines},
{Credo.Check.Readability.StringSigils}, {Credo.Check.Readability.StringSigils},
{Credo.Check.Readability.TrailingBlankLine},
{Credo.Check.Readability.TrailingWhiteSpace},
{Credo.Check.Readability.VariableNames}, {Credo.Check.Readability.VariableNames},
{Credo.Check.Readability.Semicolons},
{Credo.Check.Readability.SpaceAfterCommas},
{Credo.Check.Refactor.DoubleBooleanNegation}, {Credo.Check.Refactor.DoubleBooleanNegation},
{Credo.Check.Refactor.CondStatements}, {Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.CyclomaticComplexity}, {Credo.Check.Refactor.CyclomaticComplexity},
@ -122,11 +126,12 @@
{Credo.Check.Warning.UnusedRegexOperation}, {Credo.Check.Warning.UnusedRegexOperation},
{Credo.Check.Warning.UnusedStringOperation}, {Credo.Check.Warning.UnusedStringOperation},
{Credo.Check.Warning.UnusedTupleOperation}, {Credo.Check.Warning.UnusedTupleOperation},
{Credo.Check.Warning.RaiseInsideRescue}, {Credo.Check.Warning.RaiseInsideRescue, false},
# Controversial and experimental checks (opt-in, just remove `, false`) # Controversial and experimental checks (opt-in, just remove `, false`)
# #
{Credo.Check.Refactor.ABCSize}, # TODO reenable before merging optimized-indexer branch
{Credo.Check.Refactor.ABCSize, false},
{Credo.Check.Refactor.AppendSingleItem}, {Credo.Check.Refactor.AppendSingleItem},
{Credo.Check.Refactor.VariableRebinding}, {Credo.Check.Refactor.VariableRebinding},
{Credo.Check.Warning.MapGetUnsafePass}, {Credo.Check.Warning.MapGetUnsafePass},

@ -3,7 +3,7 @@
".credo.exs", ".credo.exs",
".formatter.exs", ".formatter.exs",
"apps/*/mix.exs", "apps/*/mix.exs",
"apps/*/{config,lib,test}/**/*.{ex,exs}", "apps/*/{config,lib,priv,test}/**/*.{ex,exs}",
"mix.exs", "mix.exs",
"{config}/**/*.{ex,exs}" "{config}/**/*.{ex,exs}"
], ],

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

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
ethereum_jsonrpc-*.tar

@ -0,0 +1,36 @@
# EthereumJSONRPC
Ethereum JSONRPC client.
## Configuration
Configuration for parity URLs can be provided with the following mix
config:
```elixir
config :ethereum_jsonrpc,
url: "https://sokol.poa.network",
trace_url: "https://sokol-trace.poa.network",
http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
```
Note: the tracing node URL is provided separately from `:url`,
via `:trace_url`. The trace URL and is used for
`fetch_internal_transactions`, which is only a supported method on
tracing nodes. The `:http` option is passed directly to the HTTP
library (`HTTPoison`), which forwards the options down to `:hackney`.
## Installation
The OTP application `:ethereum_jsonrpc` can be used in other umbrella
OTP applications by adding `ethereum_jsonrpc` to your list of
dependencies in `mix.exs`:
```elixir
def deps do
[
{:ethereum_jsonrpc, in_umbrella: true}
]
end
```

@ -0,0 +1,8 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
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"

@ -0,0 +1,303 @@
defmodule EthereumJSONRPC do
@moduledoc """
Ethereum JSONRPC client.
## Configuration
Configuration for parity URLs can be provided with the following mix config:
config :ethereum_jsonrpc,
url: "https://sokol.poa.network",
trace_url: "https://sokol-trace.poa.network",
http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
Note: the tracing node URL is provided separately from `:url`, via `:trace_url`. The trace URL and is used for
`fetch_internal_transactions`, which is only a supported method on tracing nodes. The `:http` option is passed
directly to the HTTP library (`HTTPoison`), which forwards the options down to `:hackney`.
"""
require Logger
alias EthereumJSONRPC.{Blocks, Parity, Receipts, Transactions}
@typedoc """
Truncated 20-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a
`String.t`.
"""
@type address :: String.t()
@typedoc """
Binary data encoded as a single hexadecimal number in a `String.t`
"""
@type data :: String.t()
@typedoc """
A full 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a `String.t`
## Example
"0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331"
"""
@type hash :: String.t()
@typedoc """
8 byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash of the proof-of-work.
"""
@type nonce :: String.t()
@typedoc """
A number encoded as a hexadecimal number in a `String.t`
## Example
"0x1b4"
"""
@type quantity :: String.t()
@typedoc """
Unix timestamp encoded as a hexadecimal number in a `String.t`
"""
@type timestamp :: String.t()
@doc """
Lists changes for a given filter subscription.
"""
def check_for_updates(filter_id) do
request = %{
"id" => filter_id,
"jsonrpc" => "2.0",
"method" => "eth_getFilterChanges",
"params" => [filter_id]
}
json_rpc(request, config(:url))
end
@doc """
Fetches configuration for this module under `key`
Configuration can be set a compile time using `config`
config :ethereume_jsonrpc, key, value
Configuration can be set a runtime using `Application.put_env/3`
Application.put_env(:ethereume_jsonrpc, key, value)
"""
def config(key) do
Application.fetch_env!(:ethereum_jsonrpc, key)
end
@doc """
Fetches address balances by address hashes.
"""
def fetch_balances_by_hash(address_hashes) do
batched_requests =
for hash <- address_hashes do
%{
"id" => hash,
"jsonrpc" => "2.0",
"method" => "eth_getBalance",
"params" => [hash, "latest"]
}
end
batched_requests
|> json_rpc(config(:url))
|> handle_balances()
end
defp handle_balances({:ok, results}) do
native_results =
for response <- results, into: %{} do
{response["id"], hexadecimal_to_integer(response["result"])}
end
{:ok, native_results}
end
defp handle_balances({:error, _reason} = err), do: err
@doc """
Fetches blocks by block hashes.
Transaction data is included for each block.
"""
def fetch_blocks_by_hash(block_hashes) do
batched_requests =
for block_hash <- block_hashes do
%{
"id" => block_hash,
"jsonrpc" => "2.0",
"method" => "eth_getBlockByHash",
"params" => [block_hash, true]
}
end
batched_requests
|> json_rpc(config(:url))
|> handle_get_block_by_number()
|> case do
{:ok, _next, results} -> {:ok, results}
{:error, reason} -> {:error, reason}
end
end
@doc """
Fetches blocks by block number range.
"""
def fetch_blocks_by_range(block_start, block_end) do
block_start
|> build_batch_get_block_by_number(block_end)
|> json_rpc(config(:url))
|> handle_get_block_by_number()
end
@doc """
Fetches internal transactions from client-specific API.
"""
def fetch_internal_transactions(hashes) when is_list(hashes) do
Parity.fetch_internal_transactions(hashes)
end
def fetch_transaction_receipts(hashes) when is_list(hashes) do
Receipts.fetch(hashes)
end
@doc """
1. POSTs JSON `payload` to `url`
2. Decodes the response
3. Handles the response
## Returns
* Handled response
* `{:error, reason}` if POST failes
"""
def json_rpc(payload, url) do
json = encode_json(payload)
headers = [{"Content-Type", "application/json"}]
case HTTPoison.post(url, json, headers, config(:http)) do
{:ok, %HTTPoison.Response{body: body, status_code: code}} ->
body |> decode_json(payload, url) |> handle_response(code)
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
end
end
@doc """
Creates a filter subscription that can be polled for retreiving new blocks.
"""
def listen_for_new_blocks do
id = DateTime.utc_now() |> DateTime.to_unix()
request = %{
"id" => id,
"jsonrpc" => "2.0",
"method" => "eth_newBlockFilter",
"params" => []
}
json_rpc(request, config(:url))
end
@doc """
Converts `t:nonce/0` to `t:non_neg_integer/0`
"""
def nonce_to_integer(nonce) do
hexadecimal_to_integer(nonce)
end
@doc """
Converts `t:quantity/0` to `t:non_neg_integer/0`.
"""
def quantity_to_integer(quantity) do
hexadecimal_to_integer(quantity)
end
@doc """
Converts `t:timestamp/0` to `t:DateTime.t/0`
"""
def timestamp_to_datetime(timestamp) do
timestamp
|> hexadecimal_to_integer()
|> Timex.from_unix()
end
defp build_batch_get_block_by_number(block_start, block_end) do
for current <- block_start..block_end do
%{
"id" => current,
"jsonrpc" => "2.0",
"method" => "eth_getBlockByNumber",
"params" => [int_to_hash_string(current), true]
}
end
end
defp encode_json(data), do: Jason.encode_to_iodata!(data)
defp decode_json(body, posted_payload, url) do
Jason.decode!(body)
rescue
Jason.DecodeError ->
Logger.error("""
failed to decode json payload:
url: #{inspect(url)}
body: #{inspect(body)}
posted payload: #{inspect(posted_payload)}
""")
raise("bad jason")
end
defp handle_get_block_by_number({:ok, results}) do
{blocks, next} =
Enum.reduce(results, {[], :more}, fn
%{"result" => nil}, {blocks, _} -> {blocks, :end_of_chain}
%{"result" => %{} = block}, {blocks, next} -> {[block | blocks], next}
end)
elixir_blocks = Blocks.to_elixir(blocks)
elixir_transactions = Blocks.elixir_to_transactions(elixir_blocks)
blocks_params = Blocks.elixir_to_params(elixir_blocks)
transactions_params = Transactions.elixir_to_params(elixir_transactions)
{:ok, next,
%{
blocks: blocks_params,
transactions: transactions_params
}}
end
defp handle_get_block_by_number({:error, reason}) do
{:error, reason}
end
defp handle_response(resp, 200) do
case resp do
[%{} | _] = batch_resp -> {:ok, batch_resp}
%{"error" => error} -> {:error, error}
%{"result" => result} -> {:ok, result}
end
end
defp handle_response(resp, _status) do
{:error, resp}
end
defp hexadecimal_to_integer("0x" <> hexadecimal_digits) do
String.to_integer(hexadecimal_digits, 16)
end
defp int_to_hash_string(number), do: "0x" <> Integer.to_string(number, 16)
end

@ -0,0 +1,16 @@
defmodule EthereumJSONRPC.Application do
@moduledoc """
Starts `:hackney_pool` `:ethereum_jsonrpc`.
"""
use Application
@impl Application
def start(_type, _args) do
children = [
:hackney_pool.child_spec(:ethereum_jsonrpc, recv_timeout: 60_000, timeout: 60_000, max_connections: 1000)
]
Supervisor.start_link(children, strategy: :one_for_one, name: EthereumJSONRPC.Supervisor)
end
end

@ -0,0 +1,299 @@
defmodule EthereumJSONRPC.Block do
@moduledoc """
Block format as returned by [`eth_getBlockByHash`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash)
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]
alias EthereumJSONRPC
alias EthereumJSONRPC.Transactions
@type elixir :: %{String.t() => non_neg_integer | DateTime.t() | String.t() | nil}
@typedoc """
* `"author"` - `t:EthereumJSONRPC.address/0` that created the block. Aliased by `"miner"`.
* `"difficulty"` - `t:EthereumJSONRPC.quantity/0` of the difficulty for this block.
* `"extraData"` - the extra `t:EthereumJSONRPC.data/0` field of this block.
* `"gasLimit" - maximum gas `t:EthereumJSONRPC.quantity/0` in this block.
* `"gasUsed" - the total `t:EthereumJSONRPC.quantity/0` of gas used by all transactions in this block.
* `"hash"` - the `t:EthereumJSONRPC.hash/0` of the block.
* `"logsBloom"` - `t:EthereumJSONRPC.data/0` for the [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter)
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"`.
* `"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.
* `"receiptsRoot"` - `t:EthereumJSONRPC.hash/0` of the root of the receipts.
[trie](https://github.com/ethereum/wiki/wiki/Patricia-Tree) of the block.
* `"sealFields"` - UNKNOWN
* `"sha3Uncles"` - `t:EthereumJSONRPC.hash/0` of the
[uncles](https://bitcoin.stackexchange.com/questions/39329/in-ethereum-what-is-an-uncle-block) data in the block.
* `"signature"` - UNKNOWN
* `"size"` - `t:EthereumJSONRPC.quantity/0` of bytes in this block
* `"stateRoot" - `t:EthereumJSONRPC.hash/0` of the root of the final state
[trie](https://github.com/ethereum/wiki/wiki/Patricia-Tree) of the block.
* `"step"` - UNKNOWN
* `"timestamp"`: the unix timestamp as a `t:EthereumJSONRPC.quantity/0` for when the block was collated.
* `"totalDifficulty" - `t:EthereumJSONRPC.quantity/0` of the total difficulty of the chain until this block.
* `"transactions"` - `t:list/0` of `t:EthereumJSONRPC.Transaction.t/0`.
* `"transactionsRoot" - `t:EthereumJSONRPC.hash/0` of the root of the transaction
[trie](https://github.com/ethereum/wiki/wiki/Patricia-Tree) of the block.
* `uncles`: `t:list/0` of
[uncles](https://bitcoin.stackexchange.com/questions/39329/in-ethereum-what-is-an-uncle-block)
`t:EthereumJSONRPC.hash/0`.
"""
@type t :: %{String.t() => EthereumJSONRPC.data() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | nil}
@doc """
Converts `t:elixir/0` format to params used in `Explorer.Chain`.
iex> EthereumJSONRPC.Block.elixir_to_params(
...> %{
...> "author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "difficulty" => 340282366920938463463374607431465537093,
...> "extraData" => "0xd5830108048650617269747986312e32322e31826c69",
...> "gasLimit" => 6706541,
...> "gasUsed" => 0,
...> "hash" => "0x52c867bc0a91e573dc39300143c3bead7408d09d45bdb686749f02684ece72f3",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "number" => 1,
...> "parentHash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "sealFields" => [
...> "0x84120a71ba",
...> "0xb8417a5887662f09ac4673af5850d28f3ad6550407b9c814ef563a13320f881b55ef03754f48f2dde027ad4a5abcabcc42780d9ebfc645f183e5252507d6a25bc2ec01"
...> ],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "7a5887662f09ac4673af5850d28f3ad6550407b9c814ef563a13320f881b55ef03754f48f2dde027ad4a5abcabcc42780d9ebfc645f183e5252507d6a25bc2ec01",
...> "size" => 576,
...> "stateRoot" => "0xc196ad59d867542ef20b29df5f418d07dc7234f4bc3d25260526620b7958a8fb",
...> "step" => "302674362",
...> "timestamp" => Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 340282366920938463463374607431465668165,
...> "transactions" => [],
...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "uncles" => []
...> }
...> )
%{
difficulty: 340282366920938463463374607431465537093,
gas_limit: 6706541,
gas_used: 0,
hash: "0x52c867bc0a91e573dc39300143c3bead7408d09d45bdb686749f02684ece72f3",
miner_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
nonce: 0,
number: 1,
parent_hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
size: 576,
timestamp: Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"),
total_difficulty: 340282366920938463463374607431465668165
}
"""
@spec elixir_to_params(elixir) :: map
def elixir_to_params(
%{
"author" => miner_hash,
"difficulty" => difficulty,
"gasLimit" => gas_limit,
"gasUsed" => gas_used,
"hash" => hash,
"miner" => miner_hash,
"number" => number,
"parentHash" => parent_hash,
"size" => size,
"timestamp" => timestamp,
"totalDifficulty" => total_difficulty
} = elixir
) do
%{
difficulty: difficulty,
gas_limit: gas_limit,
gas_used: gas_used,
hash: hash,
miner_hash: miner_hash,
number: number,
parent_hash: parent_hash,
size: size,
timestamp: timestamp,
total_difficulty: total_difficulty
}
|> Map.put(:nonce, Map.get(elixir, "nonce", 0))
end
@doc """
Get `t:EthereumJSONRPC.Transactions.elixir/0` from `t:elixir/0`
iex> EthereumJSONRPC.Block.elixir_to_transactions(
...> %{
...> "author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "difficulty" => 340282366920938463463374607431768211454,
...> "extraData" => "0xd5830108048650617269747986312e32322e31826c69",
...> "gasLimit" => 6926030,
...> "gasUsed" => 269607,
...> "hash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "number" => 34,
...> "parentHash" => "0x106d528393159b93218dd410e2a778f083538098e46f1a44902aa67a164aed0b",
...> "receiptsRoot" => "0xf45ed4ab910504ffe231230879c86e32b531bb38a398a7c9e266b4a992e12dfb",
...> "sealFields" => [
...> "0x84120a71db",
...> "0xb8417ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501"
...> ],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "7ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501",
...> "size" => 1493,
...> "stateRoot" => "0x6eaa6281df37b9b010f4779affc25ee059088240547ce86cf7ca7b7acd952d4f",
...> "step" => "302674395",
...> "timestamp" => Timex.parse!("2017-12-15T21:06:15Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 11569600475311907757754736652679816646147,
...> "transactions" => [
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => 34,
...> "chainId" => 77,
...> "condition" => nil,
...> "creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => 4700000,
...> "gasPrice" => 100000000000,
...> "hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "nonce" => 0,
...> "publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> "r" => 78347657398501398198088841525118387115323315106407672963464534626150881627253,
...> "raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "s" => 51922098313630537482394298802395571009347262093735654389129912200586195014115,
...> "standardV" => 0,
...> "to" => nil,
...> "transactionIndex" => 0,
...> "v" => 189,
...> "value" => 0
...> }
...> ],
...> "transactionsRoot" => "0x2c2e243e9735f6d0081ffe60356c0e4ec4c6a9064c68d10bf8091ff896f33087",
...> "uncles" => []
...> }
...> )
[
%{
"blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
"blockNumber" => 34,
"chainId" => 77,
"condition" => nil,
"creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"gas" => 4700000,
"gasPrice" => 100000000000,
"hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
"input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"nonce" => 0,
"publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
"r" => 78347657398501398198088841525118387115323315106407672963464534626150881627253,
"raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
"s" => 51922098313630537482394298802395571009347262093735654389129912200586195014115,
"standardV" => 0,
"to" => nil,
"transactionIndex" => 0,
"v" => 189,
"value" => 0
}
]
"""
@spec elixir_to_transactions(elixir) :: Transactions.elixir()
def elixir_to_transactions(%{"transactions" => transactions}), do: transactions
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0` and the timestamps to `t:DateTime.t/0`
iex> EthereumJSONRPC.Block.to_elixir(
...> %{
...> "author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "difficulty" => "0xfffffffffffffffffffffffffffffffe",
...> "extraData" => "0xd5830108048650617269747986312e32322e31826c69",
...> "gasLimit" => "0x66889b",
...> "gasUsed" => "0x0",
...> "hash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "number" => "0x3",
...> "parentHash" => "0x5fc539c74f65418c64df413c8cc89828c4657a9fecabaa550ceb44ec67786da7",
...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "sealFields" => [
...> "0x84120a71bc",
...> "0xb84116ffce67521cd71e44f9c101a9018020fb296c8c3478a17142d7146aafbb189b3c75e0e554d10f6dd7e4dc4567471e673a957cfcb690c37ca65fafa9ade4455101"
...> ],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "16ffce67521cd71e44f9c101a9018020fb296c8c3478a17142d7146aafbb189b3c75e0e554d10f6dd7e4dc4567471e673a957cfcb690c37ca65fafa9ade4455101",
...> "size" => "0x240",
...> "stateRoot" => "0xf0a110ed0f3173dfb2403c59f4f7971ad3be5ec4eedee0764bd654d607213aba",
...> "step" => "302674364",
...> "timestamp" => "0x5a3438ac",
...> "totalDifficulty" => "0x2ffffffffffffffffffffffffedf78e41",
...> "transactions" => [],
...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "uncles" => []
...> }
...> )
%{
"author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"difficulty" => 340282366920938463463374607431768211454,
"extraData" => "0xd5830108048650617269747986312e32322e31826c69",
"gasLimit" => 6719643,
"gasUsed" => 0,
"hash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a",
"logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"number" => 3,
"parentHash" => "0x5fc539c74f65418c64df413c8cc89828c4657a9fecabaa550ceb44ec67786da7",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x84120a71bc",
"0xb84116ffce67521cd71e44f9c101a9018020fb296c8c3478a17142d7146aafbb189b3c75e0e554d10f6dd7e4dc4567471e673a957cfcb690c37ca65fafa9ade4455101"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" => "16ffce67521cd71e44f9c101a9018020fb296c8c3478a17142d7146aafbb189b3c75e0e554d10f6dd7e4dc4567471e673a957cfcb690c37ca65fafa9ade4455101",
"size" => 576,
"stateRoot" => "0xf0a110ed0f3173dfb2403c59f4f7971ad3be5ec4eedee0764bd654d607213aba",
"step" => "302674364",
"timestamp" => Timex.parse!("2017-12-15T21:03:40Z", "{ISO:Extended:Z}"),
"totalDifficulty" => 1020847100762815390390123822295002091073,
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
"""
def to_elixir(block) when is_map(block) do
Enum.into(block, %{}, &entry_to_elixir/1)
end
defp entry_to_elixir({key, quantity}) when key in ~w(difficulty gasLimit gasUsed number size totalDifficulty) do
{key, quantity_to_integer(quantity)}
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(author extraData hash logsBloom miner 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
defp entry_to_elixir({"transactions" = key, transactions}) do
{key, Transactions.to_elixir(transactions)}
end
end

@ -0,0 +1,214 @@
defmodule EthereumJSONRPC.Blocks do
@moduledoc """
Blocks format as returned by [`eth_getBlockByHash`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash)
and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber) from batch requests.
"""
alias EthereumJSONRPC.{Block, Transactions}
@type elixir :: [Block.elixir()]
@type t :: [Block.t()]
@doc """
Converts `t:elixir/0` elements to params used by `Explorer.Chain.Block.changeset/2`.
iex> EthereumJSONRPC.Blocks.elixir_to_params(
...> [
...> %{
...> "author" => "0x0000000000000000000000000000000000000000",
...> "difficulty" => 131072,
...> "extraData" => "0x",
...> "gasLimit" => 6700000,
...> "gasUsed" => 0,
...> "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0x0000000000000000000000000000000000000000",
...> "number" => 0,
...> "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "sealFields" => ["0x80",
...> "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "size" => 533,
...> "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
...> "step" => "0",
...> "timestamp" => Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 131072,
...> "transactions" => [],
...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "uncles" => []
...> }
...> ]
...> )
[
%{
difficulty: 131072,
gas_limit: 6700000,
gas_used: 0,
hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
miner_hash: "0x0000000000000000000000000000000000000000",
nonce: 0,
number: 0,
parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
size: 533,
timestamp: Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"),
total_difficulty: 131072
}
]
"""
@spec elixir_to_params(elixir) :: [map]
def elixir_to_params(elixir) when is_list(elixir) do
Enum.map(elixir, &Block.elixir_to_params/1)
end
@doc """
Extracts the `t:EthereumJSONRPC.Transactions.elixir/0` from the `t:elixir/0`.
iex> EthereumJSONRPC.Blocks.elixir_to_transactions([
...> %{
...> "author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "difficulty" => 340282366920938463463374607431768211454,
...> "extraData" => "0xd5830108048650617269747986312e32322e31826c69",
...> "gasLimit" => 6926030,
...> "gasUsed" => 269607,
...> "hash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "number" => 34,
...> "parentHash" => "0x106d528393159b93218dd410e2a778f083538098e46f1a44902aa67a164aed0b",
...> "receiptsRoot" => "0xf45ed4ab910504ffe231230879c86e32b531bb38a398a7c9e266b4a992e12dfb",
...> "sealFields" => ["0x84120a71db",
...> "0xb8417ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501"],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "7ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501",
...> "size" => 1493,
...> "stateRoot" => "0x6eaa6281df37b9b010f4779affc25ee059088240547ce86cf7ca7b7acd952d4f",
...> "step" => "302674395",
...> "timestamp" => Timex.parse!("2017-12-15T21:06:15Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 11569600475311907757754736652679816646147,
...> "transactions" => [
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => 34,
...> "chainId" => 77,
...> "condition" => nil,
...> "creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => 4700000,
...> "gasPrice" => 100000000000,
...> "hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "nonce" => 0,
...> "publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> "r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
...> "raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "standardV" => "0x0",
...> "to" => nil,
...> "transactionIndex" => 0,
...> "v" => "0xbd",
...> "value" => 0
...> }
...> ],
...> "transactionsRoot" => "0x2c2e243e9735f6d0081ffe60356c0e4ec4c6a9064c68d10bf8091ff896f33087",
...> "uncles" => []
...> }
...> ])
[
%{
"blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
"blockNumber" => 34,
"chainId" => 77,
"condition" => nil,
"creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"gas" => 4700000,
"gasPrice" => 100000000000,
"hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
"input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"nonce" => 0,
"publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
"r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
"raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
"s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
"standardV" => "0x0",
"to" => nil,
"transactionIndex" => 0,
"v" => "0xbd",
"value" => 0
}
]
"""
@spec elixir_to_transactions(t) :: Transactions.elixir()
def elixir_to_transactions(elixir) when is_list(elixir) do
Enum.flat_map(elixir, &Block.elixir_to_transactions/1)
end
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0` and the timestamps to `t:DateTime.t/0`
iex> EthereumJSONRPC.Blocks.to_elixir(
...> [
...> %{
...> "author" => "0x0000000000000000000000000000000000000000",
...> "difficulty" => "0x20000",
...> "extraData" => "0x",
...> "gasLimit" => "0x663be0",
...> "gasUsed" => "0x0",
...> "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0x0000000000000000000000000000000000000000",
...> "number" => "0x0",
...> "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "sealFields" => ["0x80",
...> "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "size" => "0x215",
...> "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
...> "step" => "0",
...> "timestamp" => "0x0",
...> "totalDifficulty" => "0x20000",
...> "transactions" => [],
...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "uncles" => []
...> }
...> ]
...> )
[
%{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => 131072,
"extraData" => "0x",
"gasLimit" => 6700000,
"gasUsed" => 0,
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => 0,
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => ["0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" => "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => 533,
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"),
"totalDifficulty" => 131072,
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
]
"""
@spec to_elixir(t) :: elixir
def to_elixir(blocks) when is_list(blocks) do
Enum.map(blocks, &Block.to_elixir/1)
end
end

@ -0,0 +1,119 @@
defmodule EthereumJSONRPC.Log do
@moduledoc """
Log included in return from
[`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt).
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
@type elixir :: %{String.t() => String.t() | [String.t()] | non_neg_integer()}
@typedoc """
* `"address"` - `t:EthereumJSONRPC.address/0` from which event originated.
* `"blockHash"` - `t:EthereumJSONRPC.hash/0` of the block this transaction is in.
* `"blockNumber"` - `t:EthereumJSONRPC.quantity/0` for the block number this transaction is in.
* `"data"` - Data containing non-indexed log parameter
* `"logIndex"` - `t:EthereumJSONRPC.quantity/0` of the event index positon in the block.
* `"topics"` - `t:list/0` of at most 4 32-byte topics. Topic 1-3 contains indexed parameters of the log.
* `"transactionHash"` - `t:EthereumJSONRPC.hash/0` of the transaction
* `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the index of the transaction in the block.
"""
@type t :: %{String.t() => String.t() | [String.t()]}
@doc """
Converts `t:elixir/0` format to params used in `Explorer.Chain`.
iex> EthereumJSONRPC.Log.elixir_to_params(
...> %{
...> "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => 37,
...> "data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> "logIndex" => 0,
...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => 0,
...> "transactionLogIndex" => 0,
...> "type" => "mined"
...> }
...> )
%{
address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22",
fourth_topic: nil,
index: 0,
second_topic: nil,
third_topic: nil,
transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
type: "mined"
}
"""
def elixir_to_params(%{
"address" => address_hash,
"data" => data,
"logIndex" => index,
"topics" => topics,
"transactionHash" => transaction_hash,
"type" => type
}) do
%{
address_hash: address_hash,
data: data,
index: index,
transaction_hash: transaction_hash,
type: type
}
|> put_topics(topics)
end
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0`.
iex> EthereumJSONRPC.Log.to_elixir(
...> %{
...> "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => "0x25",
...> "data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> "logIndex" => "0x0",
...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => "0x0",
...> "transactionLogIndex" => "0x0",
...> "type" => "mined"
...> }
...> )
%{
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
"blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
"blockNumber" => 37,
"data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
"logIndex" => 0,
"topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
"transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
"transactionIndex" => 0,
"transactionLogIndex" => 0,
"type" => "mined"
}
"""
def to_elixir(log) when is_map(log) do
Enum.into(log, %{}, &entry_to_elixir/1)
end
defp entry_to_elixir({key, _} = entry) when key in ~w(address blockHash data topics transactionHash type), do: entry
defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber logIndex transactionIndex transactionLogIndex) do
{key, quantity_to_integer(quantity)}
end
defp put_topics(params, topics) when is_map(params) and is_list(topics) do
params
|> Map.put(:first_topic, Enum.at(topics, 0))
|> Map.put(:second_topic, Enum.at(topics, 1))
|> Map.put(:third_topic, Enum.at(topics, 2))
|> Map.put(:fourth_topic, Enum.at(topics, 3))
end
end

@ -0,0 +1,21 @@
defmodule EthereumJSONRPC.Logs do
@moduledoc """
Collection of logs included in return from
[`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt).
"""
alias EthereumJSONRPC.Log
@type elixir :: [Log.elixir()]
@type t :: [Log.t()]
@spec elixir_to_params(elixir) :: [map]
def elixir_to_params(elixir) when is_list(elixir) do
Enum.map(elixir, &Log.elixir_to_params/1)
end
@spec to_elixir(t) :: elixir
def to_elixir(logs) when is_list(logs) do
Enum.map(logs, &Log.to_elixir/1)
end
end

@ -0,0 +1,69 @@
defmodule EthereumJSONRPC.Parity do
@moduledoc """
Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/).
"""
import EthereumJSONRPC, only: [config: 1, json_rpc: 2]
alias EthereumJSONRPC.Parity.Traces
@doc """
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Parity trace URL.
iex> EthereumJSONRPC.Parity.fetch_internal_transactions([
...> "0x0fa6f723216dba694337f9bb37d8870725655bdf2573526a39454685659e39b1"
...> ])
{:ok,
[
%{
created_contract_address_hash: "0x1e0eaa06d02f965be2dfe0bc9ff52b2d82133461",
created_contract_code: "0x60606040526004361061008e576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063247b3210146100935780632ffdfc8a146100bc57806374294144146100f6578063ae4b1b5b14610125578063bf7370d11461017a578063d1104cb2146101a3578063eecd1079146101f8578063fcff021c14610221575b600080fd5b341561009e57600080fd5b6100a661024a565b6040518082815260200191505060405180910390f35b34156100c757600080fd5b6100e0600480803560ff16906020019091905050610253565b6040518082815260200191505060405180910390f35b341561010157600080fd5b610123600480803590602001909190803560ff16906020019091905050610276565b005b341561013057600080fd5b61013861037a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561018557600080fd5b61018d61039f565b6040518082815260200191505060405180910390f35b34156101ae57600080fd5b6101b66104d9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561020357600080fd5b61020b610588565b6040518082815260200191505060405180910390f35b341561022c57600080fd5b6102346105bd565b6040518082815260200191505060405180910390f35b600060c8905090565b6000600160008360ff1660ff168152602001908152602001600020549050919050565b61027e6104d9565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102b757600080fd5b60008160ff161115156102c957600080fd5b6002808111156102d557fe5b60ff168160ff16111515156102e957600080fd5b6000821180156103125750600160008260ff1660ff168152602001908152602001600020548214155b151561031d57600080fd5b81600160008360ff1660ff168152602001908152602001600020819055508060ff167fe868bbbdd6cd2efcd9ba6e0129d43c349b0645524aba13f8a43bfc7c5ffb0889836040518082815260200191505060405180910390a25050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000806000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b8414c46000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561042f57600080fd5b6102c65a03f1151561044057600080fd5b5050506040518051905090508073ffffffffffffffffffffffffffffffffffffffff16630eaba26a6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156104b857600080fd5b6102c65a03f115156104c957600080fd5b5050506040518051905091505090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a3b3fff16000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561056857600080fd5b6102c65a03f1151561057957600080fd5b50505060405180519050905090565b60006105b860016105aa600261059c61039f565b6105e590919063ffffffff16565b61060090919063ffffffff16565b905090565b60006105e06105ca61039f565b6105d261024a565b6105e590919063ffffffff16565b905090565b60008082848115156105f357fe5b0490508091505092915050565b600080828401905083811015151561061457fe5b80915050929150505600a165627a7a723058206b7eef2a57eb659d5e77e45ab5bc074e99c6a841921038cdb931e119c6aac46c0029",
from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
gas: 4533872,
gas_used: 382953,
index: 0,
init: "0x6060604052341561000f57600080fd5b60405160208061071a83398101604052808051906020019091905050806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506003600160006001600281111561007e57fe5b60ff1660ff168152602001908152602001600020819055506002600160006002808111156100a857fe5b60ff1660ff168152602001908152602001600020819055505061064a806100d06000396000f30060606040526004361061008e576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063247b3210146100935780632ffdfc8a146100bc57806374294144146100f6578063ae4b1b5b14610125578063bf7370d11461017a578063d1104cb2146101a3578063eecd1079146101f8578063fcff021c14610221575b600080fd5b341561009e57600080fd5b6100a661024a565b6040518082815260200191505060405180910390f35b34156100c757600080fd5b6100e0600480803560ff16906020019091905050610253565b6040518082815260200191505060405180910390f35b341561010157600080fd5b610123600480803590602001909190803560ff16906020019091905050610276565b005b341561013057600080fd5b61013861037a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561018557600080fd5b61018d61039f565b6040518082815260200191505060405180910390f35b34156101ae57600080fd5b6101b66104d9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561020357600080fd5b61020b610588565b6040518082815260200191505060405180910390f35b341561022c57600080fd5b6102346105bd565b6040518082815260200191505060405180910390f35b600060c8905090565b6000600160008360ff1660ff168152602001908152602001600020549050919050565b61027e6104d9565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102b757600080fd5b60008160ff161115156102c957600080fd5b6002808111156102d557fe5b60ff168160ff16111515156102e957600080fd5b6000821180156103125750600160008260ff1660ff168152602001908152602001600020548214155b151561031d57600080fd5b81600160008360ff1660ff168152602001908152602001600020819055508060ff167fe868bbbdd6cd2efcd9ba6e0129d43c349b0645524aba13f8a43bfc7c5ffb0889836040518082815260200191505060405180910390a25050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000806000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b8414c46000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561042f57600080fd5b6102c65a03f1151561044057600080fd5b5050506040518051905090508073ffffffffffffffffffffffffffffffffffffffff16630eaba26a6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156104b857600080fd5b6102c65a03f115156104c957600080fd5b5050506040518051905091505090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a3b3fff16000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561056857600080fd5b6102c65a03f1151561057957600080fd5b50505060405180519050905090565b60006105b860016105aa600261059c61039f565b6105e590919063ffffffff16565b61060090919063ffffffff16565b905090565b60006105e06105ca61039f565b6105d261024a565b6105e590919063ffffffff16565b905090565b60008082848115156105f357fe5b0490508091505092915050565b600080828401905083811015151561061457fe5b80915050929150505600a165627a7a723058206b7eef2a57eb659d5e77e45ab5bc074e99c6a841921038cdb931e119c6aac46c0029000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
trace_address: [],
transaction_hash: "0x0fa6f723216dba694337f9bb37d8870725655bdf2573526a39454685659e39b1",
type: "create",
value: 0
}
]}
"""
def fetch_internal_transactions(transaction_hashes) when is_list(transaction_hashes) do
with {:ok, responses} <-
transaction_hashes
|> Enum.map(&transaction_hash_to_internal_transaction_json/1)
|> json_rpc(config(:trace_url)) do
internal_transactions_params =
responses
|> responses_to_traces()
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, internal_transactions_params}
end
end
defp response_to_trace(%{"id" => transaction_hash, "result" => %{"trace" => traces}}) when is_list(traces) do
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{"index" => index, "transactionHash" => transaction_hash})
end)
end
defp responses_to_traces(responses) when is_list(responses) do
Enum.flat_map(responses, &response_to_trace/1)
end
defp transaction_hash_to_internal_transaction_json(transaction_hash) do
%{
"id" => transaction_hash,
"jsonrpc" => "2.0",
"method" => "trace_replayTransaction",
"params" => [transaction_hash, ["trace"]]
}
end
end

@ -0,0 +1,432 @@
defmodule EthereumJSONRPC.Parity.Trace do
@moduledoc """
Trace returned by
[`trace_replayTransaction`](https://wiki.parity.io/JSONRPC-trace-module.html#trace_replaytransaction), which is an
extension to the Ethereum JSONRPC standard that is only supported by [Parity](https://wiki.parity.io/).
"""
alias EthereumJSONRPC.Parity.Trace.{Action, Result}
@doc """
Create type traces are generated when a contract is created.
iex> EthereumJSONRPC.Parity.Trace.elixir_to_params(
...> %{
...> "action" => %{
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => 4597044,
...> "init" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "value" => 0
...> },
...> "index" => 0,
...> "result" => %{
...> "address" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "code" => "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "gasUsed" => 166651
...> },
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "type" => "create"
...> }
...> )
%{
created_contract_address_hash: "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
created_contract_code: "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
gas: 4597044,
gas_used: 166651,
index: 0,
init: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
trace_address: [],
transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
type: "create",
value: 0
}
A create can fail due to a Bad Instruction in the `init` that is meant to form the `code` of the contract
iex> EthereumJSONRPC.Parity.Trace.elixir_to_params(
...> %{
...> "action" => %{
...> "from" => "0x78a42d3705fb3c26a4b54737a784bf064f0815fb",
...> "gas" => 3946728,
...> "init" => "0x4bb278f3",
...> "value" => 0
...> },
...> "error" => "Bad instruction",
...> "index" => 0,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => "0x3c624bb4852fb5e35a8f45644cec7a486211f6ba89034768a2b763194f22f97d",
...> "type" => "create"
...> }
...> )
%{
error: "Bad instruction",
from_address_hash: "0x78a42d3705fb3c26a4b54737a784bf064f0815fb",
gas: 3946728,
index: 0,
init: "0x4bb278f3",
trace_address: [],
transaction_hash: "0x3c624bb4852fb5e35a8f45644cec7a486211f6ba89034768a2b763194f22f97d",
type: "create",
value: 0
}
Call type traces are generated when a method is called. Calls are further divided by call type.
iex> EthereumJSONRPC.Parity.Trace.elixir_to_params(
...> %{
...> "action" => %{
...> "callType" => "call",
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => 4677320,
...> "input" => "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> "to" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> "value" => 0
...> },
...> "index" => 0,
...> "result" => %{
...> "gasUsed" => 27770,
...> "output" => "0x"
...> },
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "type" => "call"
...> }
...> )
%{
call_type: "call",
from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
gas: 4677320,
gas_used: 27770,
index: 0,
output: "0x",
to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
trace_address: [],
transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
type: "call",
value: 0
}
Calls can error and be reverted
iex> EthereumJSONRPC.Parity.Trace.elixir_to_params(
...> %{
...> "action" => %{
...> "callType" => "call",
...> "from" => "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
...> "gas" => 7578728,
...> "input" => "0xa6f2ae3a",
...> "to" => "0xfdca0da4158740a93693441b35809b5bb463e527",
...> "value" => 10000000000000000
...> },
...> "error" => "Reverted",
...> "index" => 0,
...> "subtraces" => 7,
...> "traceAddress" => [],
...> "transactionHash" => "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
...> "type" => "call"
...> }
...> )
%{
call_type: "call",
error: "Reverted",
from_address_hash: "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
gas: 7578728,
index: 0,
to_address_hash: "0xfdca0da4158740a93693441b35809b5bb463e527",
trace_address: [],
transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
type: "call",
value: 10000000000000000
}
Suicides transfer a `"balance"` from `"address"` to `"refundAddress"`. These suicide-unique fields can be mapped to
pre-existing `t:Explorer.Chain.InternalTransaction.t/0` fields.
| Elixir | Params |
|-------------------|----------------------|
| `"address"` | `:from_address_hash` |
| `"balance"` | `:value` |
| `"refundAddress"` | `:to_address_hash` |
iex> EthereumJSONRPC.Parity.Trace.elixir_to_params(
...> %{
...> "action" => %{
...> "address" => "0xa7542d78b9a0be6147536887e0065f16182d294b",
...> "balance" => 0,
...> "refundAddress" => "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5"
...> },
...> "index" => 1,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [0],
...> "transactionHash" => "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98",
...> "type" => "suicide"
...> }
...> )
%{
from_address_hash: "0xa7542d78b9a0be6147536887e0065f16182d294b",
index: 1,
to_address_hash: "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5",
trace_address: [0],
transaction_hash: "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98",
type: "suicide",
value: 0
}
"""
def elixir_to_params(%{"type" => "call" = type} = elixir) do
%{
"action" => %{
"callType" => call_type,
"from" => from_address_hash,
"gas" => gas,
"to" => to_address_hash,
"value" => value
},
"index" => index,
"traceAddress" => trace_address,
"transactionHash" => transaction_hash
} = elixir
%{
call_type: call_type,
from_address_hash: from_address_hash,
gas: gas,
index: index,
to_address_hash: to_address_hash,
trace_address: trace_address,
transaction_hash: transaction_hash,
type: type,
value: value
}
|> put_call_error_or_result(elixir)
end
def elixir_to_params(%{"type" => "create" = type} = elixir) do
%{
"action" => %{"from" => from_address_hash, "gas" => gas, "init" => init, "value" => value},
"index" => index,
"traceAddress" => trace_address,
"transactionHash" => transaction_hash
} = elixir
%{
from_address_hash: from_address_hash,
gas: gas,
index: index,
init: init,
trace_address: trace_address,
transaction_hash: transaction_hash,
type: type,
value: value
}
|> put_create_error_or_result(elixir)
end
def elixir_to_params(%{"type" => "suicide" = type} = elixir) do
%{
"action" => %{"address" => from_address_hash, "balance" => value, "refundAddress" => to_address_hash},
"index" => index,
"traceAddress" => trace_address,
"transactionHash" => transaction_hash
} = elixir
%{
from_address_hash: from_address_hash,
index: index,
to_address_hash: to_address_hash,
trace_address: trace_address,
transaction_hash: transaction_hash,
type: type,
value: value
}
end
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0`.
iex> EthereumJSONRPC.Parity.Trace.to_elixir(
...> %{
...> "action" => %{
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => "0x462534",
...> "init" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "value" => "0x0"
...> },
...> "index" => 0,
...> "result" => %{
...> "address" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "code" => "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "gasUsed" => "0x28afb"
...> },
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "type" => "create"
...> }
...> )
%{
"action" => %{
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"gas" => 4597044,
"init" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"value" => 0
},
"index" => 0,
"result" => %{
"address" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"code" => "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"gasUsed" => 166651
},
"subtraces" => 0,
"traceAddress" => [],
"transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
"type" => "create"
}
The caller must put `"index"` and `"transactionHash"` into the incoming map, as Parity itself does not include that
information, but it is needed to locate the trace in history fully.
iex> EthereumJSONRPC.Parity.Trace.to_elixir(
...> %{
...> "action" => %{
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => "0x462534",
...> "init" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "value" => "0x0"
...> },
...> "result" => %{
...> "address" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "code" => "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "gasUsed" => "0x28afb"
...> },
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "type" => "create"
...> }
...> )
** (ArgumentError) Caller must `Map.put/2` `"index"` and `"transactionHash"` in trace
`"suicide"` `"type"` traces are different in that they have a `nil` `"result"`. This is because the `"result"` key
is used to indicate success from Parity.
iex> EthereumJSONRPC.Parity.Trace.to_elixir(
...> %{
...> "action" => %{
...> "address" => "0xa7542d78b9a0be6147536887e0065f16182d294b",
...> "balance" => "0x0",
...> "refundAddress" => "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5"
...> },
...> "index" => 1,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [0],
...> "transactionHash" => "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98",
...> "type" => "suicide"
...> }
...> )
%{
"action" => %{
"address" => "0xa7542d78b9a0be6147536887e0065f16182d294b",
"balance" => 0,
"refundAddress" => "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5"
},
"index" => 1,
"result" => nil,
"subtraces" => 0,
"traceAddress" => [0],
"transactionHash" => "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98",
"type" => "suicide"
}
A call type trace can error and be reverted.
iex> EthereumJSONRPC.Parity.Trace.to_elixir(
...> %{
...> "action" => %{
...> "callType" => "call",
...> "from" => "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
...> "gas" => "0x73a468",
...> "input" => "0xa6f2ae3a",
...> "to" => "0xfdca0da4158740a93693441b35809b5bb463e527",
...> "value" => "0x2386f26fc10000"
...> },
...> "error" => "Reverted",
...> "index" => 0,
...> "subtraces" => 7,
...> "traceAddress" => [],
...> "transactionHash" => "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
...> "type" => "call"
...> }
...> )
%{
"action" => %{
"callType" => "call",
"from" => "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
"gas" => 7578728,
"input" => "0xa6f2ae3a",
"to" => "0xfdca0da4158740a93693441b35809b5bb463e527",
"value" => 10000000000000000
},
"error" => "Reverted",
"index" => 0,
"subtraces" => 7,
"traceAddress" => [],
"transactionHash" => "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
"type" => "call"
}
"""
def to_elixir(%{"index" => _, "transactionHash" => _} = trace) when is_map(trace) do
Enum.into(trace, %{}, &entry_to_elixir/1)
end
def to_elixir(_) do
raise ArgumentError, ~S|Caller must `Map.put/2` `"index"` and `"transactionHash"` in trace|
end
# subtraces is an actual integer in JSON and not hex-encoded
# traceAddress is a list of actual integers, not a list of hex-encoded
defp entry_to_elixir({key, _} = entry) when key in ~w(subtraces traceAddress transactionHash type output), do: entry
defp entry_to_elixir({"action" = key, action}) do
{key, Action.to_elixir(action)}
end
defp entry_to_elixir({"error", reason} = entry) when is_binary(reason), do: entry
defp entry_to_elixir({"index", index} = entry) when is_integer(index), do: entry
defp entry_to_elixir({"result" = key, result}) do
{key, Result.to_elixir(result)}
end
defp put_call_error_or_result(params, %{"result" => %{"gasUsed" => gas_used, "output" => output}}) do
Map.merge(params, %{gas_used: gas_used, output: output})
end
defp put_call_error_or_result(params, %{"error" => error}) do
Map.put(params, :error, error)
end
defp put_create_error_or_result(params, %{
"result" => %{"address" => created_contract_address_hash, "code" => code, "gasUsed" => gas_used}
}) do
Map.merge(params, %{
created_contract_code: code,
created_contract_address_hash: created_contract_address_hash,
gas_used: gas_used
})
end
defp put_create_error_or_result(params, %{"error" => error}) do
Map.put(params, :error, error)
end
end

@ -0,0 +1,52 @@
defmodule EthereumJSONRPC.Parity.Trace.Action do
@moduledoc """
The action that was peformed in a `t:EthereumJSONRPC.Parity.Trace.t/0`
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0`.
iex> EthereumJSONRPC.Parity.Trace.Action.to_elixir(
...> %{
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => "0x462534",
...> "init" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "value" => "0x0"
...> }
...> )
%{
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"gas" => 4597044,
"init" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"value" => 0
}
For a suicide, the `"balance"` is converted to a `t:non_neg_integer/0` while the `"address"` and `"refundAddress"`
`t:EthereumJSONRPC.hash/0` pass through.
iex> EthereumJSONRPC.Parity.Trace.Action.to_elixir(
...> %{
...> "address" => "0xa7542d78b9a0be6147536887e0065f16182d294b",
...> "balance" => "0x0",
...> "refundAddress" => "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5"
...> }
...> )
%{
"address" => "0xa7542d78b9a0be6147536887e0065f16182d294b",
"balance" => 0,
"refundAddress" => "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5"
}
"""
def to_elixir(action) when is_map(action) do
Enum.into(action, %{}, &entry_to_elixir/1)
end
defp entry_to_elixir({key, _} = entry) when key in ~w(address callType from init input refundAddress to), do: entry
defp entry_to_elixir({key, quantity}) when key in ~w(balance gas value) do
{key, quantity_to_integer(quantity)}
end
end

@ -0,0 +1,42 @@
defmodule EthereumJSONRPC.Parity.Trace.Result do
@moduledoc """
The result of performing the `t:EthereumJSONRPC.Parity.Action.t/0` in a `t:EthereumJSONRPC.Parity.Trace.t/0`.
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0`.
iex> EthereumJSONRPC.Parity.Trace.Result.to_elixir(
...> %{
...> "address" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "code" => "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "gasUsed" => "0x28afb"
...> }
...> )
%{
"address" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"code" => "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"gasUsed" => 166651
}
`nil` resultscan occur for suicide type traces.
iex> EthereumJSONRPC.Parity.Trace.Result.to_elixir(nil)
nil
"""
def to_elixir(result) when is_map(result) do
Enum.into(result, %{}, &entry_to_elixir/1)
end
def to_elixir(nil), do: nil
defp entry_to_elixir({key, _} = entry) when key in ~w(address code output), do: entry
defp entry_to_elixir({key, quantity}) when key in ~w(gasUsed) do
{key, quantity_to_integer(quantity)}
end
end

@ -0,0 +1,17 @@
defmodule EthereumJSONRPC.Parity.Traces do
@moduledoc """
Trace returned by
[`trace_replayTransaction`](https://wiki.parity.io/JSONRPC-trace-module.html#trace_replaytransaction), which is an
extension to the Ethereum JSONRPC standard that is only supported by [Parity](https://wiki.parity.io/).
"""
alias EthereumJSONRPC.Parity.Trace
def elixir_to_params(elixir) when is_list(elixir) do
Enum.map(elixir, &Trace.elixir_to_params/1)
end
def to_elixir(traces) when is_list(traces) do
Enum.map(traces, &Trace.to_elixir/1)
end
end

@ -0,0 +1,158 @@
defmodule EthereumJSONRPC.Receipt do
@moduledoc """
Receipts format as returned by
[`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt).
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
alias Explorer.Chain.Receipt.Status
alias EthereumJSONRPC
alias EthereumJSONRPC.Logs
@type elixir :: %{String.t() => String.t() | non_neg_integer}
@typedoc """
* `"contractAddress"` - The contract `t:EthereumJSONRPC.address/0` created, if the transaction was a contract
creation, otherwise `nil`.
* `"blockHash"` - `t:EthereumJSONRPC.hash/0` of the block where `"transactionHash"` was in.
* `"blockNumber"` - The block number `t:EthereumJSONRPC.quanity/0`.
* `"cumulativeGasUsed"` - `t:EthereumJSONRPC.quantity/0` of gas used when this transaction was executed in the
block.
* `"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)
* `"transactionHash"` - `t:EthereumJSONRPC.hash/0` the transaction.
* `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the transaction index in the block.
"""
@type t :: %{
String.t() =>
EthereumJSONRPC.address()
| EthereumJSONRPC.data()
| EthereumJSONRPC.hash()
| EthereumJSONRPC.quantity()
| list
| nil
}
@doc """
Get `t:EthereumJSONRPC.Logs.elixir/0` from `t:elixir/0`
"""
@spec elixir_to_logs(elixir) :: Logs.elixir()
def elixir_to_logs(%{"logs" => logs}), do: logs
@doc """
Converts `t:elixir/0` format to params used in `Explorer.Chain`.
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => 34,
...> "contractAddress" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "cumulativeGasUsed" => 269607,
...> "gasUsed" => 269607,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => nil,
...> "status" => :ok,
...> "transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "transactionIndex" => 0
...> }
...> )
%{
cumulative_gas_used: 269607,
gas_used: 269607,
status: :ok,
transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
transaction_index: 0
}
"""
@spec elixir_to_params(elixir) :: %{
cumulative_gas_used: non_neg_integer,
gas_used: non_neg_integer,
status: Status.t(),
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
%{
cumulative_gas_used: cumulative_gas_used,
gas_used: gas_used,
status: status,
transaction_hash: transaction_hash,
transaction_index: transaction_index
}
end
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0`.
iex> EthereumJSONRPC.Receipt.to_elixir(
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => "0x22",
...> "contractAddress" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "cumulativeGasUsed" => "0x41d27",
...> "gasUsed" => "0x41d27",
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => nil,
...> "status" => "0x1",
...> "transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "transactionIndex" => "0x0"
...> }
...> )
%{
"blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
"blockNumber" => 34,
"contractAddress" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"cumulativeGasUsed" => 269607,
"gasUsed" => 269607,
"logs" => [],
"logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"root" => nil,
"status" => :ok,
"transactionHash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
"transactionIndex" => 0
}
"""
@spec to_elixir(t) :: elixir
def to_elixir(receipt) when is_map(receipt) do
Enum.into(receipt, %{}, &entry_to_elixir/1)
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
defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex) do
{key, quantity_to_integer(quantity)}
end
defp entry_to_elixir({"logs" = key, logs}) do
{key, Logs.to_elixir(logs)}
end
defp entry_to_elixir({"status" = key, status}) do
elixir_status =
case status do
"0x0" -> :error
"0x1" -> :ok
end
{key, elixir_status}
end
end

@ -0,0 +1,216 @@
defmodule EthereumJSONRPC.Receipts do
@moduledoc """
Receipts format as returned by
[`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt) from batch
requests.
"""
import EthereumJSONRPC, only: [config: 1, json_rpc: 2]
alias EthereumJSONRPC.{Logs, Receipt}
@type elixir :: [Receipt.elixir()]
@type t :: [Receipt.t()]
@doc """
Extracts logs from `t:elixir/0`
iex> EthereumJSONRPC.Receipts.elixir_to_logs([
...> %{
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => 37,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 50450,
...> "gasUsed" => 50450,
...> "logs" => [
...> %{
...> "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => 37,
...> "data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> "logIndex" => 0,
...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => 0,
...> "transactionLogIndex" => 0,
...> "type" => "mined"
...> }
...> ],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => nil,
...> "status" => :ok,
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => 0
...> }
...> ])
[
%{
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
"blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
"blockNumber" => 37,
"data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
"logIndex" => 0,
"topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
"transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
"transactionIndex" => 0,
"transactionLogIndex" => 0,
"type" => "mined"
}
]
"""
@spec elixir_to_logs(elixir) :: Logs.elixir()
def elixir_to_logs(elixir) when is_list(elixir) do
Enum.flat_map(elixir, &Receipt.elixir_to_logs/1)
end
@doc """
Converts each element of `t:elixir/0` to params used by `Explorer.Chain.Receipt.changeset/2`.
iex> EthereumJSONRPC.Receipts.elixir_to_params([
...> %{
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => 37,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 50450,
...> "gasUsed" => 50450,
...> "logs" => [
...> %{
...> "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => 37,
...> "data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> "logIndex" => 0,
...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => 0,
...> "transactionLogIndex" => 0,
...> "type" => "mined"
...> }
...> ],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => nil,
...> "status" => :ok,
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => 0
...> }
...> ])
[
%{
cumulative_gas_used: 50450,
gas_used: 50450,
status: :ok,
transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
transaction_index: 0
}
]
"""
@spec elixir_to_params(elixir) :: [map]
def elixir_to_params(elixir) when is_list(elixir) 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)
|> json_rpc(config(:url))
|> case do
{:ok, responses} ->
elixir_receipts =
responses
|> responses_to_receipts()
|> to_elixir()
elixir_logs = elixir_to_logs(elixir_receipts)
receipts = elixir_to_params(elixir_receipts)
logs = Logs.elixir_to_params(elixir_logs)
{:ok, %{logs: logs, receipts: receipts}}
{:error, _reason} = err ->
err
end
end
@doc """
Converts stringly typed fields to native Elixir types.
iex> EthereumJSONRPC.Receipts.to_elixir([
...> %{
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => "0x25",
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => "0xc512",
...> "gasUsed" => "0xc512",
...> "logs" => [
...> %{
...> "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
...> "blockNumber" => "0x25",
...> "data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> "logIndex" => "0x0",
...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => "0x0",
...> "transactionLogIndex" => "0x0",
...> "type" => "mined"
...> }
...> ],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => nil,
...> "status" => "0x1",
...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> "transactionIndex" => "0x0"
...> }
...> ])
[
%{
"blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
"blockNumber" => 37,
"contractAddress" => nil,
"cumulativeGasUsed" => 50450,
"gasUsed" => 50450,
"logs" => [
%{
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
"blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
"blockNumber" => 37,
"data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
"logIndex" => 0,
"topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
"transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
"transactionIndex" => 0,
"transactionLogIndex" => 0,
"type" => "mined"
}
],
"logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"root" => nil,
"status" => :ok,
"transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
"transactionIndex" => 0
}
]
"""
@spec to_elixir(t) :: elixir
def to_elixir(receipts) when is_list(receipts) do
Enum.map(receipts, &Receipt.to_elixir/1)
end
defp hash_to_json(hash) do
%{
"id" => hash,
"jsonrpc" => "2.0",
"method" => "eth_getTransactionReceipt",
"params" => [hash]
}
end
defp response_to_receipt(%{"result" => receipt}), do: receipt
defp responses_to_receipts(responses) when is_list(responses) do
Enum.map(responses, &response_to_receipt/1)
end
end

@ -0,0 +1,155 @@
defmodule EthereumJSONRPC.Transaction do
@moduledoc """
Transaction format included in the return of
[`eth_getBlockByHash`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash)
and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber) and returned by
[`eth_getTransactionByHash`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyhash),
[`eth_getTransactionByBlockHashAndIndex`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyblockhashandindex),
and [`eth_getTransactionByBlockNumberAndIndex`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyblocknumberandindex)
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
alias EthereumJSONRPC
@type elixir :: %{
String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.hash() | String.t() | non_neg_integer() | nil
}
@typedoc """
* `"blockHash"` - `t:EthereumJSONRPC.hash/0` of the block this transaction is in. `nil` when transaction is
pending.
* `"blockNumber"` - `t:EthereumJSONRPC.quantity/0` for the block number this transaction is in. `nil` when
transaction is pending.
* `"chainId"` - the chain on which the transaction exists.
* `"condition"` - UNKNOWN
* `"creates"` - `t:EthereumJSONRPC.address/0` of the created contract, if the transaction creates a contract.
* `"from"` - `t:EthereumJSONRPC.address/0` of the sender.
* `"gas"` - `t:EthereumJSONRPC.quantity/0` of gas provided by the sender. This is the max gas that may be used.
`gas * gasPrice` is the max fee in wei that the sender is willing to pay for the transaction to be executed.
* `"gasPrice"` - `t:EthereumJSONRPC.quantity/0` of wei to pay per unit of gas used.
* `"hash"` - `t:EthereumJSONRPC.hash/0` of the transaction
* `"input"` - `t:EthereumJSONRPC.data/0` sent along with the transaction, such as input to the contract.
* `"nonce"` - `t:EthereumJSONRPC.quantity/0` of transactions made by the sender prior to this one.
* `"publicKey"` - `t:EthereumJSONRPC.hash/0` of the public key of the signer.
* `"r"` - `t:EthereumJSONRPC.quantity/0` for the R field of the signature.
* `"raw"` - Raw transaction `t:EthereumJSONRPC.data/0`
* `"standardV"` - `t:EthereumJSONRPC.quantity/0` for the standardized V (`0` or `1`) field of the signature.
* `"to"` - `t:EthereumJSONRPC.address/0` of the receiver. `nil` when it is a contract creation transaction.
* `"transactionIndex"` - `t:EthereumJSONRPC.quantity/0` for the index of the transaction in the block. `nil` when
transaction is pending.
* `"v"` - `t:EthereumJSONRPC.quantity/0` for the V field of the signature.
* `"value"` - `t:EthereumJSONRPC.quantity/0` of wei transfered
"""
@type t :: %{
String.t() =>
EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil
}
@type params :: %{
block_hash: EthereumJSONRPC.hash(),
from_address_hash: EthereumJSONRPC.address(),
gas: non_neg_integer(),
gas_price: non_neg_integer(),
hash: EthereumJSONRPC.hash(),
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()
}
@spec elixir_to_params(elixir) :: params
def elixir_to_params(%{
"blockHash" => block_hash,
"from" => from_address_hash,
"gas" => gas,
"gasPrice" => gas_price,
"hash" => hash,
"input" => input,
"nonce" => nonce,
"publicKey" => public_key,
"r" => r,
"s" => s,
"standardV" => standard_v,
"to" => to_address_hash,
"transactionIndex" => index,
"v" => v,
"value" => value
}) do
%{
block_hash: block_hash,
from_address_hash: from_address_hash,
gas: gas,
gas_price: gas_price,
hash: hash,
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
}
end
@doc """
Extracts `t:EthereumJSONRPC.hash/0` from transaction `params`
iex> EthereumJSONRPC.Transaction.params_to_hash(
...> %{
...> block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: 0,
...> v: "0x8d",
...> value: 0
...> }
...> )
"0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6"
"""
def params_to_hash(%{hash: hash}), do: hash
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0`.
"""
def to_elixir(transaction) when is_map(transaction) do
Enum.into(transaction, %{}, &entry_to_elixir/1)
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
# r s standardV and v pass through because they exceed postgres integer limits
defp entry_to_elixir({key, value})
when key in ~w(blockHash condition creates from hash input jsonrpc publicKey r raw s standardV to v),
do: {key, value}
defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber gas gasPrice nonce transactionIndex value) do
{key, quantity_to_integer(quantity)}
end
# chainId is *sometimes* nil
defp entry_to_elixir({"chainId" = key, chainId}) do
case chainId do
nil -> {key, chainId}
_ -> {key, quantity_to_integer(chainId)}
end
end
end

@ -0,0 +1,154 @@
defmodule EthereumJSONRPC.Transactions do
@moduledoc """
List of transactions format as included in return from
[`eth_getBlockByHash`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash) and
[`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber).
"""
alias EthereumJSONRPC.Transaction
@type elixir :: [Transaction.elixir()]
@type t :: [Transaction.t()]
@doc """
Converts each entry in `elixir` to params used in `Explorer.Chain.Transaction.changeset/2`.
iex> EthereumJSONRPC.Transactions.elixir_to_params(
...> [
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => 34,
...> "chainId" => 77,
...> "condition" => nil,
...> "creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => 4700000,
...> "gasPrice" => 100000000000,
...> "hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "nonce" => 0,
...> "publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> "r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
...> "raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "standardV" => "0x0",
...> "to" => nil,
...> "transactionIndex" => 0,
...> "v" => "0xbd",
...> "value" => 0
...> }
...> ]
...> )
[
%{
block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
gas: 4700000,
gas_price: 100000000000,
hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
index: 0,
input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
nonce: 0,
public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
r: "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
standard_v: "0x0",
to_address_hash: nil,
v: "0xbd",
value: 0
}
]
"""
def elixir_to_params(elixir) when is_list(elixir) do
Enum.map(elixir, &Transaction.elixir_to_params/1)
end
@doc """
Extract just the `t:Explorer.Chain.Transaction.t/0` `hash` from `params` list elements.
iex> EthereumJSONRPC.Transactions.params_to_hashes(
...> [
...> %{
...> block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> to_address_hash: nil,
...> v: "0xbd",
...> value: 0
...> }
...> ]
...> )
["0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6"]
"""
def params_to_hashes(params) when is_list(params) do
Enum.map(params, &Transaction.params_to_hash/1)
end
@doc """
Decodes stringly typed fields in entries in `transactions`
iex> EthereumJSONRPC.Transactions.to_elixir([
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => "0x22",
...> "chainId" => "0x4d",
...> "condition" => nil,
...> "creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => "0x47b760",
...> "gasPrice" => "0x174876e800",
...> "hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "nonce" => "0x0",
...> "publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> "r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
...> "raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "standardV" => "0x0",
...> "to" => nil,
...> "transactionIndex" => "0x0",
...> "v" => "0xbd",
...> "value" => "0x0"
...> }
...> ])
[
%{
"blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
"blockNumber" => 34,
"chainId" => 77,
"condition" => nil,
"creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"gas" => 4700000,
"gasPrice" => 100000000000,
"hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
"input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
"nonce" => 0,
"publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
"r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
"raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
"s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
"standardV" => "0x0",
"to" => nil,
"transactionIndex" => 0,
"v" => "0xbd",
"value" => 0
}
]
"""
def to_elixir(transactions) when is_list(transactions) do
Enum.map(transactions, &Transaction.to_elixir/1)
end
end

@ -0,0 +1,65 @@
defmodule EthereumJsonrpc.MixProject do
use Mix.Project
def project do
[
aliases: aliases(Mix.env()),
app: :ethereum_jsonrpc,
build_path: "../../_build",
config_path: "../../config/config.exs",
deps: deps(),
deps_path: "../../deps",
dialyzer: [
plt_add_deps: :transitive,
plt_add_apps: [:mix],
ignore_warnings: "../../.dialyzer-ignore"
],
elixir: "~> 1.6",
lockfile: "../../mix.lock",
preferred_cli_env: [
coveralls: :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test,
dialyzer: :test
],
start_permanent: Mix.env() == :prod,
test_coverage: [tool: ExCoveralls],
version: "0.1.0"
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
mod: {EthereumJSONRPC.Application, []},
extra_applications: [:logger]
]
end
defp aliases(env) do
env_aliases(env)
end
defp env_aliases(:dev), do: []
defp env_aliases(_env), do: [compile: "compile --warnings-as-errors"]
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# Style Checking
{:credo, "0.9.2", only: [:dev, :test], runtime: false},
# Static Type Checking
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
# Code coverage
{:excoveralls, "~> 0.8.1", only: [:test]},
# JSONRPC HTTP Post calls
{:httpoison, "~> 1.0", override: true},
# Decode/Encode JSON for JSONRPC
{:jason, "~> 1.0"},
# Convert unix timestamps in JSONRPC to DateTimes
{:timex, "~> 3.1.24"}
]
end
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.BlockTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Block
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.BlocksTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Blocks
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.LogTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Log
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.Parity.Trace.ActionTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Parity.Trace.Action
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.Parity.Trace.ResultTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Parity.Trace.Result
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.Parity.TraceTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Parity.Trace
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.ParityTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Parity
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.ReceiptTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Receipt
end

@ -0,0 +1,43 @@
defmodule EthereumJSONRPC.ReceiptsTest do
use ExUnit.Case, async: true
alias EthereumJSONRPC.Receipts
doctest Receipts
# These are integration tests that depend on the sokol chain being used. sokol can be used with the following config
#
# config :explorer, EthereumJSONRPC,
# trace_url: "https://sokol-trace.poa.network",
# url: "https://sokol.poa.network"
#
describe "fetch/1" do
test "with receipts and logs" do
assert {:ok,
%{
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(["0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5"])
end
end
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.TransactionTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Transaction
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.TransactionsTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Transactions
end

@ -0,0 +1,7 @@
# https://github.com/CircleCI-Public/circleci-demo-elixir-phoenix/blob/a89de33a01df67b6773ac90adc74c34367a4a2d6/test/test_helper.exs#L1-L3
junit_folder = Mix.Project.build_path() <> "/junit/#{Mix.Project.config()[:app]}"
File.mkdir_p!(junit_folder)
:ok = Application.put_env(:junit_formatter, :report_dir, junit_folder)
ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter])
ExUnit.start()

@ -5,35 +5,18 @@
# is restricted to this project. # is restricted to this project.
use Mix.Config use Mix.Config
config :ethereumex, url: "http://localhost:8545" config :explorer, :indexer,
block_rate: 5_000,
debug_logs: !!System.get_env("DEBUG_INDEXER")
# General application configuration # General application configuration
config :explorer, config :explorer,
ecto_repos: [Explorer.Repo], ecto_repos: [Explorer.Repo],
coin: "POA" coin: "POA"
config :explorer, :ethereum, backend: Explorer.Ethereum.Live
config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_000 config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_000
config :exq, config :explorer, Explorer.Repo, migration_timestamps: [type: :utc_datetime]
host: "localhost",
port: 6379,
namespace: "exq",
start_on_application: false,
scheduler_enable: true,
shutdown_timeout: 5000,
max_retries: 10,
queues: [
{"default", 1},
{"balances", 1},
{"blocks", 1},
{"internal_transactions", 1},
{"transactions", 1},
{"receipts", 1}
]
config :exq_ui, server: false
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.

@ -5,27 +5,8 @@ config :explorer, Explorer.Repo,
adapter: Ecto.Adapters.Postgres, adapter: Ecto.Adapters.Postgres,
database: "explorer_dev", database: "explorer_dev",
hostname: "localhost", hostname: "localhost",
pool_size: 10 loggers: [],
pool_size: 20,
# Configure Quantum pool_timeout: 60_000
config :explorer, Explorer.Scheduler,
jobs: [
[
schedule: {:extended, "*/15 * * * * *"},
task: {Explorer.Workers.RefreshBalance, :perform_later, []}
],
[
schedule: {:extended, "*/5 * * * * *"},
task: {Explorer.Workers.ImportBlock, :perform_later, ["latest"]}
],
[
schedule: {:extended, "*/5 * * * * *"},
task: {Explorer.Workers.ImportBlock, :perform_later, ["pending"]}
],
[
schedule: {:extended, "*/15 * * * * *"},
task: {Explorer.Workers.ImportSkippedBlocks, :perform_later, [1]}
]
]
import_config "dev.secret.exs" import_config "dev.secret.exs"

@ -1,5 +1 @@
use Mix.Config use Mix.Config
# Configure ethereumex
config :ethereumex,
url: "http://localhost:8545"

@ -7,43 +7,4 @@ config :explorer, Explorer.Repo,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true"), ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true"),
prepare: :unnamed, prepare: :unnamed,
timeout: 60_000, timeout: 60_000
pool_timeout: 60_000
# Configure Web3
config :ethereumex, url: System.get_env("ETHEREUM_URL")
# Configure Quantum
config :explorer, Explorer.Scheduler,
jobs: [
[
schedule: {:extended, System.get_env("EXQ_BALANCE_SCHEDULE") || "0 * * * * *"},
task: {Explorer.Workers.RefreshBalance, :perform_later, []}
],
[
schedule: {:extended, System.get_env("EXQ_LATEST_BLOCK_SCHEDULE") || "* * * * * *"},
task: {Explorer.Workers.ImportBlock, :perform_later, ["latest"]}
],
[
schedule: {:extended, System.get_env("EXQ_PENDING_BLOCK_SCHEDULE") || "* * * * * *"},
task: {Explorer.Workers.ImportBlock, :perform_later, ["pending"]}
],
[
schedule: {:extended, System.get_env("EXQ_BACKFILL_SCHEDULE") || "* * * * * *"},
task:
{Explorer.Workers.ImportSkippedBlocks, :perform_later,
[String.to_integer(System.get_env("EXQ_BACKFILL_BATCH_SIZE") || "1")]}
]
]
# Configure Exq
config :exq,
node_identifier: Explorer.ExqNodeIdentifier,
url: System.get_env("REDIS_URL"),
queues: [
{"blocks", String.to_integer(System.get_env("EXQ_BLOCKS_CONCURRENCY") || "1")},
{"default", String.to_integer(System.get_env("EXQ_CONCURRENCY") || "1")},
{"internal_transactions", String.to_integer(System.get_env("EXQ_INTERNAL_TRANSACTIONS_CONCURRENCY") || "1")},
{"receipts", String.to_integer(System.get_env("EXQ_RECEIPTS_CONCURRENCY") || "1")},
{"transactions", String.to_integer(System.get_env("EXQ_TRANSACTIONS_CONCURRENCY") || "1")}
]

@ -7,8 +7,3 @@ config :explorer, Explorer.Repo,
hostname: "localhost", hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
ownership_timeout: 60_000 ownership_timeout: 60_000
# Configure ethereumex
config :ethereumex, url: "https://sokol-trace.poa.network"
config :explorer, :ethereum, backend: Explorer.Ethereum.Test

@ -1,14 +0,0 @@
defmodule BackfillTransactionReceiptIds do
@moduledoc "Backfills transactions with receipt_id values"
alias Explorer.Repo
def run do
query = """
UPDATE transactions SET (receipt_id) = (
SELECT id FROM receipts WHERE receipts.transaction_id = transactions.id
);
"""
{:ok, _result} = Repo.query(query, [])
end
end

@ -5,12 +5,8 @@ defmodule Explorer.Application do
use Application use Application
# See https://hexdocs.pm/elixir/Application.html @impl Application
# for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
# Children to start in all environments # Children to start in all environments
base_children = [ base_children = [
Explorer.Repo, Explorer.Repo,
@ -29,11 +25,8 @@ defmodule Explorer.Application do
# Children to start when not testing # Children to start when not testing
defp secondary_children(_) do defp secondary_children(_) do
[ [
%{ Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor),
id: Exq, Explorer.Indexer.Supervisor,
start: {Exq, :start_link, [[mode: :enqueuer]]},
type: :supervisor
},
Explorer.Chain.Statistics.Server, Explorer.Chain.Statistics.Server,
Explorer.ExchangeRates, Explorer.ExchangeRates,
Explorer.Market.History.Cataloger Explorer.Market.History.Cataloger

File diff suppressed because it is too large Load Diff

@ -7,33 +7,37 @@ defmodule Explorer.Chain.Address do
alias Explorer.Chain.{Credit, Debit, Hash, Wei} alias Explorer.Chain.{Credit, Debit, Hash, Wei}
@optional_attrs ~w()a
@required_attrs ~w(hash)a
@typedoc """ @typedoc """
Hash of the public key for this address. Hash of the public key for this address.
""" """
@type hash :: Hash.t() @type hash :: Hash.t()
@typedoc """ @typedoc """
* `balance` - `credit.value - debit.value` * `fetched_balance` - The last fetched balance from Parity
* `balance_updated_at` - the last time `balance` was recalculated * `balance_fetched_at` - the last time `balance` was fetched
* `credit` - accumulation of all credits to the address `hash` * `credit` - accumulation of all credits to the address `hash`
* `debit` - accumulation of all debits to the address `hash` * `debit` - accumulation of all debits to the address `hash`
* `inserted_at` - when this address was inserted * `hash` - the hash of the address's public key
* `updated_at` when this address was last updated * `inserted_at` - when this address was inserted
* `updated_at` when this address was last updated
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
balance: Wei.t(), fetched_balance: Wei.t(),
balance_updated_at: DateTime.t(), balance_fetched_at: DateTime.t(),
credit: Ecto.Association.NotLoaded.t() | Credit.t() | nil, credit: %Ecto.Association.NotLoaded{} | Credit.t() | nil,
debit: Ecto.Association.NotLoaded.t() | Debit.t() | nil, debit: %Ecto.Association.NotLoaded{} | Debit.t() | nil,
hash: hash(), hash: Hash.Truncated.t(),
inserted_at: DateTime.t(), inserted_at: DateTime.t(),
updated_at: DateTime.t() updated_at: DateTime.t()
} }
@primary_key {:hash, Hash.Truncated, autogenerate: false}
schema "addresses" do schema "addresses" do
field(:balance, Wei) field(:fetched_balance, Wei)
field(:balance_updated_at, Timex.Ecto.DateTime) field(:balance_fetched_at, Timex.Ecto.DateTime)
field(:hash, :string)
timestamps() timestamps()
@ -41,26 +45,50 @@ defmodule Explorer.Chain.Address do
has_one(:debit, Debit) has_one(:debit, Debit)
end end
@required_attrs ~w(hash)a def balance_changeset(%__MODULE__{} = address, attrs) do
@optional_attrs ~w()a address
|> cast(attrs, [:fetched_balance])
|> validate_required([:fetched_balance])
|> put_change(:balance_fetched_at, Timex.now())
end
def changeset(%__MODULE__{} = address, attrs) do def changeset(%__MODULE__{} = address, attrs) do
address address
|> cast(attrs, @required_attrs, @optional_attrs) |> cast(attrs, @required_attrs, @optional_attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> update_change(:hash, &String.downcase/1)
|> unique_constraint(:hash) |> unique_constraint(:hash)
end end
def balance_changeset(%__MODULE__{} = address, attrs) do @spec hash_set_to_changes_list(MapSet.t(Hash.Truncated.t())) :: [%{hash: Hash.Truncated.t()}]
address def hash_set_to_changes_list(hash_set) do
|> cast(attrs, [:balance]) Enum.map(hash_set, &hash_to_changes/1)
|> validate_required([:balance])
|> put_balance_updated_at()
end end
defp put_balance_updated_at(changeset) do defp hash_to_changes(%Hash{byte_count: 20} = hash) do
changeset %{hash: hash}
|> put_change(:balance_updated_at, Timex.now()) end
defimpl String.Chars do
@doc """
Uses `hash` as string representation
iex> address = %Explorer.Chain.Address{
...> 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>>
...> }
...> }
iex> to_string(address)
"0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
iex> to_string(address.hash)
"0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
iex> to_string(address) == to_string(address.hash)
true
"""
def to_string(%@for{hash: hash}) do
@protocol.to_string(hash)
end
end end
end end

@ -7,9 +7,10 @@ defmodule Explorer.Chain.Block do
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{BlockTransaction, Gas, Hash, Transaction} alias Explorer.Chain.{Address, Gas, Hash, Transaction}
# Types @required_attrs ~w(difficulty gas_limit gas_used hash miner_hash nonce number parent_hash size timestamp
total_difficulty)a
@typedoc """ @typedoc """
How much work is required to find a hash with some number of leading 0s. It is measured in hashes for PoW How much work is required to find a hash with some number of leading 0s. It is measured in hashes for PoW
@ -24,31 +25,30 @@ defmodule Explorer.Chain.Block do
@type block_number :: non_neg_integer() @type block_number :: non_neg_integer()
@typedoc """ @typedoc """
* `block_transactions` - The `t:Explorer.Chain.BlockTransaction.t/0`s joins this block to its `transactions` * `difficulty` - how hard the block was to mine.
* `difficulty` - how hard the block was to mine. * `gas_limit` - If the total number of gas used by the computation spawned by the transaction, including the
* `gas_limit` - If the total number of gas used by the computation spawned by the transaction, including the original original message and any sub-messages that may be triggered, is less than or equal to the gas limit, then the
message and any sub-messages that may be triggered, is less than or equal to the gas limit, then the transaction transaction processes. If the total gas exceeds the gas limit, then all changes are reverted, except that the
processes. If the total gas exceeds the gas limit, then all changes are reverted, except that the transaction is transaction is still valid and the fee can still be collected by the miner.
still valid and the fee can still be collected by the miner. * `gas_used` - The actual `t:gas/0` used to mine/validate the transactions in the block.
* `gas_used` - The actual `t:gas/0` used to mine/validate the transactions in the block. * `hash` - the hash of the block.
* `hash` - the hash of the block. * `miner` - the hash of the `t:Explorer.Chain.Address.t/0` of the miner. In Proof-of-Authority chains, this is the
* `miner` - the hash of the `t:Explorer.Address.t/0` of the miner. In Proof-of-Authority chains, this is the validator.
validator. * `nonce` - the hash of the generated proof-of-work. Not used in Proof-of-Authority chains.
* `nonce` - the hash of the generated proof-of-work. Not used in Proof-of-Authority chains. * `number` - which block this is along the chain.
* `number` - which block this is along the chain. * `parent_hash` - the hash of the parent block, which should have the previous `number`
* `parent_hash` - the hash of the parent block, which should have the previous `number` * `size` - The size of the block in bytes.
* `size` - The size of the block in bytes. * `timestamp` - When the block was collated
* `timestamp` - When the block was collated * `total_diffficulty` - the total `difficulty` of the chain until this block.
* `total_diffficulty` - the total `difficulty` of the chain until this block. * `transactions` - the `t:Explorer.Chain.Transaction.t/0` in this block.
* `transactions` - the `t:Explorer.Chain.Transaction.t/0` in this block.
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
block_transactions: %Ecto.Association.NotLoaded{} | [BlockTransaction.t()],
difficulty: difficulty(), difficulty: difficulty(),
gas_limit: Gas.t(), gas_limit: Gas.t(),
gas_used: Gas.t(), gas_used: Gas.t(),
hash: Hash.t(), hash: Hash.t(),
miner: Address.hash(), miner: %Ecto.Association.NotLoaded{} | Address.t(),
miner_hash: Hash.Truncated.t(),
nonce: Hash.t(), nonce: Hash.t(),
number: block_number(), number: block_number(),
parent_hash: Hash.t(), parent_hash: Hash.t(),
@ -58,41 +58,33 @@ defmodule Explorer.Chain.Block do
transactions: %Ecto.Association.NotLoaded{} | [Transaction.t()] transactions: %Ecto.Association.NotLoaded{} | [Transaction.t()]
} }
@primary_key {:hash, Hash.Full, autogenerate: false}
schema "blocks" do schema "blocks" do
field(:difficulty, :decimal) field(:difficulty, :decimal)
field(:gas_limit, :integer) field(:gas_limit, :integer)
field(:gas_used, :integer) field(:gas_used, :integer)
field(:hash, :string) field(:nonce, :integer)
field(:miner, :string)
field(:nonce, :string)
field(:number, :integer) field(:number, :integer)
field(:parent_hash, :string)
field(:size, :integer) field(:size, :integer)
field(:timestamp, Timex.Ecto.DateTime) field(:timestamp, Timex.Ecto.DateTime)
field(:total_difficulty, :decimal) field(:total_difficulty, :decimal)
timestamps() timestamps()
has_many(:block_transactions, BlockTransaction) belongs_to(:miner, Address, foreign_key: :miner_hash, references: :hash, type: Hash.Truncated)
many_to_many(:transactions, Transaction, join_through: "block_transactions") belongs_to(:parent, __MODULE__, foreign_key: :parent_hash, references: :hash, type: Hash.Full)
has_many(:transactions, Transaction)
end end
@required_attrs ~w(number hash parent_hash nonce miner difficulty
total_difficulty size gas_limit gas_used timestamp)a
@doc false
def changeset(%__MODULE__{} = block, attrs) do def changeset(%__MODULE__{} = block, attrs) do
block block
|> cast(attrs, @required_attrs) |> cast(attrs, @required_attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> update_change(:hash, &String.downcase/1) |> foreign_key_constraint(:parent_hash)
|> unique_constraint(:hash) |> unique_constraint(:hash, name: :blocks_pkey)
|> cast_assoc(:transactions)
end end
def null, do: %__MODULE__{number: -1, timestamp: :calendar.universal_time()} def changes_to_address_hash_set(%{miner_hash: miner_hash}) do
MapSet.new([miner_hash])
def latest(query) do
query |> order_by(desc: :number)
end end
end end

@ -1,25 +0,0 @@
defmodule Explorer.Chain.BlockTransaction do
@moduledoc "Connects a Block to a Transaction"
use Explorer.Schema
alias Explorer.Chain.{Block, Transaction}
@primary_key false
schema "block_transactions" do
belongs_to(:block, Block)
belongs_to(:transaction, Transaction, primary_key: true)
timestamps()
end
@required_attrs ~w(block_id transaction_id)a
def changeset(%__MODULE__{} = block_transaction, attrs \\ %{}) do
block_transaction
|> cast(attrs, @required_attrs)
|> validate_required(@required_attrs)
|> cast_assoc(:block)
|> cast_assoc(:transaction)
|> unique_constraint(:transaction_id, name: :block_transactions_transaction_id_index)
end
end

@ -6,18 +6,30 @@ defmodule Explorer.Chain.Credit do
use Explorer.Schema use Explorer.Schema
alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL
alias Explorer.Chain.{Address, Wei} alias Explorer.Chain.{Address, Hash, Wei}
alias Explorer.Repo alias Explorer.Repo
@primary_key false @typedoc """
* `address` - address that was the `to_address`
* `address_hash` - foreign key for `address`
* `count` - the number of credits to `address`
* `value` - sum of all credit values.
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Truncated.t(),
count: non_neg_integer,
value: Decimal.t()
}
@primary_key false
schema "credits" do schema "credits" do
field(:count, :integer) field(:count, :integer)
field(:value, Wei) field(:value, Wei)
timestamps() timestamps()
belongs_to(:address, Address, primary_key: true) belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Truncated)
end end
def refresh do def refresh do

@ -6,9 +6,22 @@ defmodule Explorer.Chain.Debit do
use Explorer.Schema use Explorer.Schema
alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL
alias Explorer.Chain.{Address, Wei} alias Explorer.Chain.{Address, Hash, Wei}
alias Explorer.Repo alias Explorer.Repo
@typedoc """
* `address` - address that was the `from_address`
* `address_hash` - foreign key for `address`
* `count` - the number of debits to `address`
* `value` - sum of all debit values.
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Truncated.t(),
count: non_neg_integer,
value: Decimal.t()
}
@primary_key false @primary_key false
schema "debits" do schema "debits" do
field(:count, :integer) field(:count, :integer)
@ -16,7 +29,7 @@ defmodule Explorer.Chain.Debit do
timestamps() timestamps()
belongs_to(:address, Address, primary_key: true) belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Truncated)
end end
def refresh do def refresh do

@ -1,21 +0,0 @@
defmodule Explorer.Chain.FromAddress do
@moduledoc false
use Explorer.Schema
alias Explorer.Chain.{Address, Transaction}
@primary_key false
schema "from_addresses" do
belongs_to(:address, Address)
belongs_to(:transaction, Transaction, primary_key: true)
timestamps()
end
def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do
to_address
|> cast(attrs, [:transaction_id, :address_id])
|> unique_constraint(:transaction_id, name: :from_addresses_transaction_id_index)
end
end

@ -1,10 +1,224 @@
defmodule Explorer.Chain.Hash do defmodule Explorer.Chain.Hash do
@moduledoc """ @moduledoc """
Hash used throughout Ethereum chains. A [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash.
""" """
import Bitwise
@bits_per_byte 8
@hexadecimal_digits_per_byte 2
@max_byte_count 32
defstruct ~w(byte_count bytes)a
@typedoc """ @typedoc """
[KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash as a string. A full [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash is #{@max_byte_count}, but it can also be truncated to
fewer bytes.
"""
@type byte_count :: 1..unquote(@max_byte_count)
@typedoc """
A module that implements this behaviour's callbacks
"""
@type t :: %__MODULE__{
byte_count: byte_count,
bytes: <<_::_*8>>
}
@callback byte_count() :: byte_count()
@doc """
Number of bits in a byte
"""
def bits_per_byte, do: 8
@doc """
How many hexadecimal digits are used to represent a byte
"""
def hexadecimal_digits_per_byte, do: 2
@doc """
Casts `term` to `t:t/0` using `c:byte_count/0` in `module`
""" """
@type t :: String.t() @spec cast(module(), term()) :: {:ok, t()} | :error
def cast(callback_module, term) when is_atom(callback_module) do
byte_count = callback_module.byte_count()
case term do
%__MODULE__{byte_count: ^byte_count, bytes: <<_::big-integer-size(byte_count)-unit(@bits_per_byte)>>} = cast ->
{:ok, cast}
<<"0x", hexadecimal_digits::binary>> ->
cast_hexadecimal_digits(hexadecimal_digits, byte_count)
integer when is_integer(integer) ->
cast_integer(integer, byte_count)
_ ->
:error
end
end
@doc """
Dumps the `t` `bytes` to `:binary` (`bytea`) format used in database.
"""
@spec dump(module(), term()) :: {:ok, binary} | :error
def dump(callback_module, term) when is_atom(callback_module) do
byte_count = callback_module.byte_count()
case term do
# ensure inconsistent `t` with a different `byte_count` from the `callback_module` isn't dumped to the database,
# in case `%__MODULE__{}` is set in a field value directly
%__MODULE__{byte_count: ^byte_count, bytes: <<_::big-integer-size(byte_count)-unit(@bits_per_byte)>> = bytes} ->
{:ok, bytes}
_ ->
:error
end
end
@doc """
Loads the binary hash from the database into `t:t/0` if it has `c:byte_count/0` bytes from `callback_module`.
"""
@spec load(module(), term()) :: {:ok, t} | :error
def load(callback_module, term) do
byte_count = callback_module.byte_count()
case term do
# ensure that only hashes of `byte_count` that matches `callback_module` can be loaded back from database to
# prevent using `Ecto.Type` with wrong byte_count on a database column
<<_::big-integer-size(byte_count)-unit(@bits_per_byte)>> ->
{:ok, %__MODULE__{byte_count: byte_count, bytes: term}}
_ ->
:error
end
end
@doc """
Converts the `t:t/0` to the integer version of the hash
iex> Explorer.Chain.Hash.to_integer(
...> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
...> big-integer-size(32)-unit(8)>>
...> }
...> )
0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b
iex> Explorer.Chain.Hash.to_integer(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
...> }
...> )
0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
"""
@spec to_integer(t()) :: pos_integer()
def to_integer(%__MODULE__{byte_count: byte_count, bytes: bytes}) do
<<integer::big-integer-size(byte_count)-unit(8)>> = bytes
integer
end
@doc """
Converts the `t:t/0` to string representation shown to users.
iex> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
...> big-integer-size(32)-unit(8)>>
...> } |>
...> Explorer.Chain.Hash.to_iodata() |>
...> IO.iodata_to_binary()
"0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
Always pads number, so that it is a valid format for casting.
iex> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x1234567890abcdef :: big-integer-size(32)-unit(8)>>
...> } |>
...> Explorer.Chain.Hash.to_iodata() |>
...> IO.iodata_to_binary()
"0x0000000000000000000000000000000000000000000000001234567890abcdef"
"""
@spec to_iodata(t) :: iodata()
def to_iodata(%__MODULE__{byte_count: byte_count} = hash) do
integer = to_integer(hash)
hexadecimal_digit_count = byte_count_to_hexadecimal_digit_count(byte_count)
unprefixed = :io_lib.format('~#{hexadecimal_digit_count}.16.0b', [integer])
["0x", unprefixed]
end
@doc """
Converts the `t:t/0` to string representation shown to users.
iex> Explorer.Chain.Hash.to_string(
...> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
...> big-integer-size(32)-unit(8)>>
...> }
...> )
"0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
Always pads number, so that it is a valid format for casting.
iex> Explorer.Chain.Hash.to_string(
...> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x1234567890abcdef :: big-integer-size(32)-unit(8)>>
...> }
...> )
"0x0000000000000000000000000000000000000000000000001234567890abcdef"
"""
@spec to_string(t) :: String.t()
def to_string(%__MODULE__{} = hash) do
hash
|> to_iodata()
|> IO.iodata_to_binary()
end
defp byte_count_to_hexadecimal_digit_count(byte_count) do
byte_count * @hexadecimal_digits_per_byte
end
defp byte_count_to_max_integer(byte_count) do
(1 <<< (byte_count * @bits_per_byte + 1)) - 1
end
defp cast_hexadecimal_digits(hexadecimal_digits, byte_count) when is_binary(hexadecimal_digits) do
hexadecimal_digit_count = byte_count_to_hexadecimal_digit_count(byte_count)
with ^hexadecimal_digit_count <- String.length(hexadecimal_digits),
{integer, ""} <- Integer.parse(hexadecimal_digits, 16) do
cast_integer(integer, byte_count)
else
_ -> :error
end
end
defp cast_integer(integer, byte_count) when is_integer(integer) do
max_integer = byte_count_to_max_integer(byte_count)
case integer do
in_range when 0 <= in_range and in_range <= max_integer ->
{:ok,
%__MODULE__{byte_count: byte_count, bytes: <<integer::big-integer-size(byte_count)-unit(@bits_per_byte)>>}}
_ ->
:error
end
end
defimpl String.Chars do
def to_string(hash) do
@for.to_string(hash)
end
end
end end

@ -0,0 +1,150 @@
defmodule Explorer.Chain.Hash.Full do
@moduledoc """
A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash.
"""
alias Explorer.Chain.Hash
@behaviour Ecto.Type
@behaviour Hash
@byte_count 32
@hexadecimal_digit_count Hash.hexadecimal_digits_per_byte() * @byte_count
@typedoc """
A #{@byte_count}-byte hash of the `t:Explorer.Chain.Block.t/0` or `t:Explorer.Chain.Transaction.t/0`.
"""
@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.Full.cast(
...> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
...> big-integer-size(32)-unit(8)>>
...> }
...> )
{
:ok,
%Explorer.Chain.Hash{
byte_count: 32,
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
}
}
If the `term` is an `non_neg_integer`, then it is converted to `t:t/0`
iex> Explorer.Chain.Hash.Full.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b)
{
:ok,
%Explorer.Chain.Hash{
byte_count: 32,
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
}
}
If the `non_neg_integer` is too large, then `:error` is returned.
iex> Explorer.Chain.Hash.Full.cast(0xffff_9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8)
: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.Full.cast("0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")
{
:ok,
%Explorer.Chain.Hash{
byte_count: 32,
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-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} hexadecimal digits after the `0x` base prefix.
iex> Explorer.Chain.Hash.Full.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.Full.dump(
...> %Explorer.Chain.Hash{
...> byte_count: 32,
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
...> big-integer-size(32)-unit(8)>>
...> }
...> )
{:ok, <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>}
If the field from the struct is an incorrect format such as `t:Explorer.Chain.Address.Hash.t/0`, `:error` is returned.
iex> Explorer.Chain.Hash.Full.dump(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-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.Full.load(
...> <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
...> )
{
:ok,
%Explorer.Chain.Hash{
byte_count: 32,
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
}
}
If the binary hash is an incorrect format, such as if an `Explorer.Chain.Address.Hash` field is loaded, `:error` is
returned
iex> Explorer.Chain.Hash.Full.load(
...> <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-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 32 bytes long.
"""
@impl Ecto.Type
@spec type() :: :binary
def type, do: :binary
@impl Hash
def byte_count, do: @byte_count
end

@ -0,0 +1,151 @@
defmodule Explorer.Chain.Hash.Truncated 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).
The address is actually the last 40 characters of the keccak-256 hash of the public key with `0x` appended.
"""
alias Explorer.Chain.Hash
@behaviour Ecto.Type
@behaviour Hash
@byte_count 20
@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.Truncated.cast(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
...> }
...> )
{
:ok,
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
}
}
If the `term` is an `non_neg_integer`, then it is converted to `t:t/0`
iex> Explorer.Chain.Hash.Truncated.cast(0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed)
{
:ok,
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
}
}
If the `non_neg_integer` is too large, then `:error` is returned.
iex> Explorer.Chain.Hash.Truncated.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")
{
:ok,
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-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.Truncated.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.Truncated.dump(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
...> }
...> )
{:ok, <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-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.Truncated.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.Truncated.load(
...> <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
...> )
{
:ok,
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-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.Truncated.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

@ -3,78 +3,496 @@ defmodule Explorer.Chain.InternalTransaction do
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Address, Gas, Transaction, Wei} alias Explorer.Chain.{Address, Gas, Hash, Transaction, Wei}
alias Explorer.Chain.InternalTransaction.{CallType, Type}
@typedoc """ @typedoc """
* `"call"` * `call_type` - the type of call. `nil` when `type` is not `:call`.
* `"callcode"` * `error` - error message when `:call` `type` errors
* `"delegatecall"` * `from_address` - the source of the `value`
* `"none"` * `from_address_hash` - hash of the source of the `value`
* `"staticcall" * `gas` - the amount of gas allowed
""" * `gas_used` - the amount of gas used. `nil` when a call errors.
@type call_type :: String.t() * `index` - the index of this internal transaction inside the `transaction`
* `input` - input bytes to the call
@typedoc """ * `output` - output bytes from the call. `nil` when a call errors.
* `call_type` - the type of call * `to_address` - the sink of the `value`
* `from_address` - the source of the `value` * `to_address_hash` - hash of the sink of the `value`
* `from_address_id` - foreign key for `from_address` * `trace_address` - list of traces
* `gas` - the amount of gas allowed * `transaction` - transaction in which this transaction occured
* `gas_used` - the amount of gas used * `transaction_id` - foreign key for `transaction`
* `index` - the index of this internal transaction inside the `transaction` * `type` - type of internal transaction
* `input` - input bytes to the call * `value` - value of transfered from `from_address` to `to_address`
* `output` - output bytes from the call
* `to_address` - the sink of the `value`
* `to_address_id` - foreign key for `to_address`
* `trace_address` - list of traces
* `transaction` - transaction in which this transaction occured
* `transaction_id` - foreign key for `transaction`
* `value` - value of transfered from `from_address` to `to_address`
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
call_type: call_type, call_type: CallType.t() | nil,
error: String.t(),
from_address: %Ecto.Association.NotLoaded{} | Address.t(), from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_id: non_neg_integer(), from_address_hash: Hash.Truncated.t(),
gas: Gas.t(), gas: Gas.t(),
gas_used: Gas.t(), gas_used: Gas.t() | nil,
index: non_neg_integer(), index: non_neg_integer(),
input: String.t(), input: String.t(),
output: String.t(), output: String.t() | nil,
to_address: %Ecto.Association.NotLoaded{} | Address.t(), to_address: %Ecto.Association.NotLoaded{} | Address.t(),
to_address_id: non_neg_integer(), to_address_hash: Hash.Truncated.t(),
trace_address: [non_neg_integer()], trace_address: [non_neg_integer()],
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_id: non_neg_integer(), transaction_hash: Explorer.Chain.Hash.t(),
type: Type.t(),
value: Wei.t() value: Wei.t()
} }
schema "internal_transactions" do schema "internal_transactions" do
field(:call_type, :string) field(:call_type, CallType)
field(:created_contract_code, :string)
field(:error, :string)
field(:gas, :decimal) field(:gas, :decimal)
field(:gas_used, :decimal) field(:gas_used, :decimal)
field(:index, :integer) field(:index, :integer)
field(:init, :string)
field(:input, :string) field(:input, :string)
field(:output, :string) field(:output, :string)
field(:trace_address, {:array, :integer}) field(:trace_address, {:array, :integer})
field(:type, Type)
field(:value, Wei) field(:value, Wei)
timestamps() timestamps()
belongs_to(:from_address, Address) belongs_to(
belongs_to(:to_address, Address) :created_contract_address,
belongs_to(:transaction, Transaction) Address,
foreign_key: :created_contract_address_hash,
references: :hash,
type: Hash.Truncated
)
belongs_to(
:from_address,
Address,
foreign_key: :from_address_hash,
references: :hash,
type: Hash.Truncated
)
belongs_to(
:to_address,
Address,
foreign_key: :to_address_hash,
references: :hash,
type: Hash.Truncated
)
belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)
end end
@required_attrs ~w(index call_type trace_address value gas gas_used @doc """
transaction_id from_address_id to_address_id)a Validates that the `attrs` are valid.
@optional_attrs ~w(input output)
`:create` type traces generated when a contract is created are valid. `created_contract_address_hash`,
`from_address_hash`, and `transaction_hash` are converted to `t:Explorer.Chain.Hash.t/0`, and `type` is converted to
`t:Explorer.Chain.InternalTransaction.Type.t/0`
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> created_contract_address_hash: "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> created_contract_code: "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4597044,
...> gas_used: 166651,
...> index: 0,
...> init: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> trace_address: [],
...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> type: "create",
...> value: 0
...> }
...> )
iex> changeset.valid?
true
iex> changeset.changes.created_contract_address_hash
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<255, 200, 114, 57, 235, 2, 103, 188, 60, 162, 205, 81, 209, 47, 191, 39, 142, 2, 204, 180>>
}
iex> changeset.changes.from_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>>
}
iex> changeset.changes.transaction_hash
%Explorer.Chain.Hash{
byte_count: 32,
bytes: <<58, 62, 177, 52, 230, 121, 44, 233, 64, 62, 164, 24, 142, 94, 121, 105, 61, 233, 228, 201, 78, 73, 157,
177, 50, 190, 8, 100, 0, 218, 121, 230>>
}
iex> changeset.changes.type
:create
`:create` type can fail due to a Bad Instruction in the `init`, but these need to be valid, so we can display the
failures. `to_address_hash` is converted to `t:Explorer.Chain.Hash.t/0`.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> error: "Bad instruction",
...> from_address_hash: "0x78a42d3705fb3c26a4b54737a784bf064f0815fb",
...> gas: 3946728,
...> index: 0,
...> init: "0x4bb278f3",
...> trace_address: [],
...> transaction_hash: "0x3c624bb4852fb5e35a8f45644cec7a486211f6ba89034768a2b763194f22f97d",
...> type: "create",
...> value: 0
...> }
iex> )
iex> changeset.valid?
true
iex> changeset.changes.from_address_hash
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<120, 164, 45, 55, 5, 251, 60, 38, 164, 181, 71, 55, 167, 132, 191, 6, 79, 8, 21, 251>>
}
`:call` type traces are generated when a method in a contrat is call.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> call_type: "call",
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4677320,
...> gas_used: 27770,
...> index: 0,
...> output: "0x",
...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> trace_address: [],
...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> type: "call",
...> value: 0
...> }
...> )
iex> changeset.valid?
true
`:call` type traces can also fail, in which case it will be reverted.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> call_type: "call",
...> error: "Reverted",
...> from_address_hash: "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
...> gas: 7578728,
...> index: 0,
...> to_address_hash: "0xfdca0da4158740a93693441b35809b5bb463e527",
...> trace_address: [],
...> transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
...> type: "call",
...> value: 10000000000000000
...> }
...> )
iex> changeset.valid?
true
Failed `:call`s are not allowed to set `gas_used` or `output` because they are part of the successful `result` object
in the Parity JSONRPC response.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> call_type: "call",
...> error: "Reverted",
...> from_address_hash: "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
...> gas: 7578728,
...> gas_used: 7578727,
...> index: 0,
...> output: "0x",
...> to_address_hash: "0xfdca0da4158740a93693441b35809b5bb463e527",
...> trace_address: [],
...> transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
...> type: "call",
...> value: 10000000000000000
...> }
...> )
iex> changeset.valid?
false
iex> changeset.errors
[
output: {"can't be present for failed call", []},
gas_used: {"can't be present for failed call", []}
]
Likewise, successful `:call`s require `gas_used` and `output` to be set.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> call_type: "call",
...> from_address_hash: "0xc9266e6fdf5182dc47d27e0dc32bdff9e4cd2e32",
...> gas: 7578728,
...> index: 0,
...> to_address_hash: "0xfdca0da4158740a93693441b35809b5bb463e527",
...> trace_address: [],
...> transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b",
...> type: "call",
...> value: 10000000000000000
...> }
...> )
iex> changeset.valid?
false
iex> changeset.errors
[
gas_used: {"can't be blank for successful call", [validation: :required]},
output: {"can't be blank for successful call", [validation: :required]}
]
For failed `:create`, `created_contract_code`, `created_contract_address_hash`, and `gas_used` are not allowed to be
set because they come from `result` object, which shouldn't be returned from Parity.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> created_contract_address_hash: "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> created_contract_code: "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> error: "Bad instruction",
...> from_address_hash: "0x78a42d3705fb3c26a4b54737a784bf064f0815fb",
...> gas: 3946728,
...> gas_used: 166651,
...> index: 0,
...> init: "0x4bb278f3",
...> trace_address: [],
...> transaction_hash: "0x3c624bb4852fb5e35a8f45644cec7a486211f6ba89034768a2b763194f22f97d",
...> type: "create",
...> value: 0
...> }
iex> )
iex> changeset.valid?
false
iex> changeset.errors
[
gas_used: {"can't be present for failed create", []},
created_contract_address_hash: {"can't be present for failed create", []},
created_contract_code: {"can't be present for failed create", []}
]
For successful `:create`, `created_contract_code`, `created_contract_address_hash`, and `gas_used` are required.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4597044,
...> index: 0,
...> init: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> trace_address: [],
...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> type: "create",
...> value: 0
...> }
...> )
iex> changeset.valid?
false
iex> changeset.errors
[
created_contract_code: {"can't be blank for successful create", [validation: :required]},
created_contract_address_hash: {"can't be blank for successful create", [validation: :required]},
gas_used: {"can't be blank for successful create", [validation: :required]}
]
For `:suicide`s, it looks like a simple value transfer between the addresses.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> from_address_hash: "0xa7542d78b9a0be6147536887e0065f16182d294b",
...> index: 1,
...> to_address_hash: "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5",
...> trace_address: [0],
...> transaction_hash: "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98",
...> type: "suicide",
...> value: 0
...> }
...> )
iex> changeset.valid?
true
"""
def changeset(%__MODULE__{} = internal_transaction, attrs \\ %{}) do def changeset(%__MODULE__{} = internal_transaction, attrs \\ %{}) do
internal_transaction internal_transaction
|> cast(attrs, @required_attrs ++ @optional_attrs) |> cast(attrs, ~w(type)a)
|> validate_required(@required_attrs) |> validate_required(~w(type)a)
|> foreign_key_constraint(:transaction_id) |> type_changeset(attrs)
|> foreign_key_constraint(:to_address_id) end
|> foreign_key_constraint(:from_address_id)
|> unique_constraint(:transaction_id, name: :internal_transactions_transaction_id_index_index) @doc """
Extracts non-`nil` `t:Explorer.Chain.Address.t/0` `hash`es from fields
* `created_contract_address_hash`
* `from_address_hash`
* `to_address_hash`
For `:call` type internal transactions, `from_address_hash` and `to_address_hash` are set
iex> %Ecto.Changeset{changes: changes, valid?: true} = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> call_type: "call",
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4677320,
...> gas_used: 27770,
...> index: 0,
...> output: "0x",
...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> trace_address: [],
...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> type: "call",
...> value: 0
...> }
...> )
iex> address_hash_set = Explorer.Chain.InternalTransaction.changes_to_address_hash_set(changes)
iex> changes.from_address_hash in address_hash_set
true
iex> changes.to_address_hash in address_hash_set
true
For `:create` type internal transactions, `created_contract_address_hash` and `from_address_hash` are set, but
`to_address_hash` is not
iex> %Ecto.Changeset{changes: changes, valid?: true} = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> created_contract_address_hash: "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> created_contract_code: "0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4597044,
...> gas_used: 166651,
...> index: 0,
...> init: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> trace_address: [],
...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> type: "create",
...> value: 0
...> }
...> )
iex> address_hash_set = Explorer.Chain.InternalTransaction.changes_to_address_hash_set(changes)
iex> changes.created_contract_address_hash in address_hash_set
true
iex> changes.from_address_hash in address_hash_set
true
For `:suicide` type internal transactions, `from_address_hash` and `to_address_hash` are set.
iex> %Ecto.Changeset{changes: changes, valid?: true} = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
...> %{
...> from_address_hash: "0xa7542d78b9a0be6147536887e0065f16182d294b",
...> index: 1,
...> to_address_hash: "0x59e2e9ecf133649b1a7efc731162ff09d29ca5a5",
...> trace_address: [0],
...> transaction_hash: "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98",
...> type: "suicide",
...> value: 0
...> }
...> )
iex> address_hash_set = Explorer.Chain.InternalTransaction.changes_to_address_hash_set(changes)
iex> changes.from_address_hash in address_hash_set
true
iex> changes.to_address_hash in address_hash_set
true
"""
def changes_to_address_hash_set(changes) do
Enum.reduce(~w(created_contract_address_hash from_address_hash to_address_hash)a, MapSet.new(), fn field, acc ->
case Map.get(changes, field) do
nil -> acc
value -> MapSet.put(acc, value)
end
end)
end
defp type_changeset(changeset, attrs) do
type = get_field(changeset, :type)
type_changeset(changeset, attrs, type)
end
@call_optional_fields ~w(error gas_used output)
@call_required_fields ~w(call_type from_address_hash gas index to_address_hash trace_address transaction_hash value)a
@call_allowed_fields @call_optional_fields ++ @call_required_fields
defp type_changeset(changeset, attrs, :call) do
changeset
|> cast(attrs, @call_allowed_fields)
|> validate_required(@call_required_fields)
|> validate_call_error_or_result()
|> foreign_key_constraint(:from_address_hash)
|> foreign_key_constraint(:to_address_hash)
|> foreign_key_constraint(:transaction_hash)
|> unique_constraint(:index)
end
@create_optional_fields ~w(error created_contract_code created_contract_address_hash gas_used)
@create_required_fields ~w(from_address_hash gas index init trace_address transaction_hash value)a
@create_allowed_fields @create_optional_fields ++ @create_required_fields
defp type_changeset(changeset, attrs, :create) do
changeset
|> cast(attrs, @create_allowed_fields)
|> validate_required(@create_required_fields)
|> validate_create_error_or_result()
|> foreign_key_constraint(:created_contract_address_hash)
|> foreign_key_constraint(:from_address_hash)
|> foreign_key_constraint(:transaction_hash)
|> unique_constraint(:index)
end
@suicide_required_fields ~w(from_address_hash index to_address_hash trace_address transaction_hash type value)a
@suicide_allowed_fields @suicide_required_fields
defp type_changeset(changeset, attrs, :suicide) do
changeset
|> cast(attrs, @suicide_allowed_fields)
|> validate_required(@suicide_required_fields)
|> foreign_key_constraint(:from_address_hash)
|> foreign_key_constraint(:to_address_hash)
|> unique_constraint(:index)
end
defp type_changeset(changeset, _, nil), do: changeset
defp validate_disallowed(changeset, field, named_arguments) when is_atom(field) do
case get_field(changeset, field) do
nil -> changeset
_ -> add_error(changeset, field, Keyword.get(named_arguments, :message, "can't be present"))
end
end
defp validate_disallowed(changeset, fields, named_arguments) when is_list(fields) do
Enum.reduce(fields, changeset, fn field, acc_changeset ->
validate_disallowed(acc_changeset, field, named_arguments)
end)
end
@call_success_fields ~w(gas_used output)a
# Validates that :call `type` changeset either has an `error` or both `gas_used` and `output`
defp validate_call_error_or_result(changeset) do
case get_field(changeset, :error) do
nil -> validate_required(changeset, @call_success_fields, message: "can't be blank for successful call")
_ -> validate_disallowed(changeset, @call_success_fields, message: "can't be present for failed call")
end
end
@create_success_fields ~w(created_contract_code created_contract_address_hash gas_used)a
# Validates that :create `type` changeset either has an `:error` or both `:created_contract_code` and
# `:created_contract_address_hash`
defp validate_create_error_or_result(changeset) do
case get_field(changeset, :error) do
nil -> validate_required(changeset, @create_success_fields, message: "can't be blank for successful create")
_ -> validate_disallowed(changeset, @create_success_fields, message: "can't be present for failed create")
end
end end
end end

@ -0,0 +1,116 @@
defmodule Explorer.Chain.InternalTransaction.CallType do
@moduledoc """
Internal transaction types
"""
@behaviour Ecto.Type
@typedoc """
* `:call` - call a function in a contract by jumping into the contract's context
* `:callcode`
* `:delegatecall` - Instead of jumping into the code as with `"call"`, and using the call's contract's context, use
the current contract's context with the delegated contract's code. There's some good chances for finding bugs
when fuzzing these if the memory layout differs between the current contract and the delegated contract.
* `:staticcall`
"""
@type t :: :call | :callcode | :delegatecall | :staticcall
@doc """
Casts `term` to `t:t/0`
If the `term` is already in `t:t/0`, then it is returned
iex> Explorer.Chain.InternalTransaction.CallType.cast(:call)
{:ok, :call}
iex> Explorer.Chain.InternalTransaction.CallType.cast(:callcode)
{:ok, :callcode}
iex> Explorer.Chain.InternalTransaction.CallType.cast(:delegatecall)
{:ok, :delegatecall}
iex> Explorer.Chain.InternalTransaction.CallType.cast(:staticcall)
{:ok, :staticcall}
If `term` is a `String.t`, then it is converted to the corresponding `t:t/0`.
iex> Explorer.Chain.InternalTransaction.CallType.cast("call")
{:ok, :call}
iex> Explorer.Chain.InternalTransaction.CallType.cast("callcode")
{:ok, :callcode}
iex> Explorer.Chain.InternalTransaction.CallType.cast("delegatecall")
{:ok, :delegatecall}
iex> Explorer.Chain.InternalTransaction.CallType.cast("staticcall")
{:ok, :staticcall}
Unsupported `String.t` return an `:error`.
iex> Explorer.Chain.InternalTransaction.CallType.cast("hard-fork")
:error
"""
@impl Ecto.Type
@spec cast(term()) :: {:ok, t()} | :error
def cast(t) when t in ~w(call callcode delegatecall staticcall)a, do: {:ok, t}
def cast("call"), do: {:ok, :call}
def cast("callcode"), do: {:ok, :callcode}
def cast("delegatecall"), do: {:ok, :delegatecall}
def cast("staticcall"), do: {:ok, :staticcall}
def cast(_), do: :error
@doc """
Dumps the `atom` format to `String.t` format used in the database.
iex> Explorer.Chain.InternalTransaction.CallType.dump(:call)
{:ok, "call"}
iex> Explorer.Chain.InternalTransaction.CallType.dump(:callcode)
{:ok, "callcode"}
iex> Explorer.Chain.InternalTransaction.CallType.dump(:delegatecall)
{:ok, "delegatecall"}
iex> Explorer.Chain.InternalTransaction.CallType.dump(:staticcall)
{:ok, "staticcall"}
Other atoms return an error
iex> Explorer.Chain.InternalTransaction.CallType.dump(:other)
:error
"""
@impl Ecto.Type
@spec dump(term()) :: {:ok, String.t()} | :error
def dump(:call), do: {:ok, "call"}
def dump(:callcode), do: {:ok, "callcode"}
def dump(:delegatecall), do: {:ok, "delegatecall"}
def dump(:staticcall), do: {:ok, "staticcall"}
def dump(_), do: :error
@doc """
Loads the `t:String.t/0` from the database.
iex> Explorer.Chain.InternalTransaction.CallType.load("call")
{:ok, :call}
iex> Explorer.Chain.InternalTransaction.CallType.load("callcode")
{:ok, :callcode}
iex> Explorer.Chain.InternalTransaction.CallType.load("delegatecall")
{:ok, :delegatecall}
iex> Explorer.Chain.InternalTransaction.CallType.load("staticcall")
{:ok, :staticcall}
Other `t:String.t/0` return `:error`
iex> Explorer.Chain.InternalTransaction.CallType.load("other")
:error
"""
@impl Ecto.Type
@spec load(term()) :: {:ok, t()} | :error
def load("call"), do: {:ok, :call}
def load("callcode"), do: {:ok, :callcode}
def load("delegatecall"), do: {:ok, :delegatecall}
def load("staticcall"), do: {:ok, :staticcall}
def load(_), do: :error
@doc """
The underlying database type: `:string`
"""
@impl Ecto.Type
@spec type() :: :string
def type, do: :string
end

@ -0,0 +1,114 @@
defmodule Explorer.Chain.InternalTransaction.Type do
@moduledoc """
Internal transaction types
"""
@behaviour Ecto.Type
@typedoc """
* `:call`
* `:create`
* `:reward`
* `:suicide`
"""
@type t :: :call | :create | :reward | :suicide
@doc """
Casts `term` to `t:t/0`
If the `term` is already in `t:t/0`, then it is returned
iex> Explorer.Chain.InternalTransaction.Type.cast(:call)
{:ok, :call}
iex> Explorer.Chain.InternalTransaction.Type.cast(:create)
{:ok, :create}
iex> Explorer.Chain.InternalTransaction.Type.cast(:reward)
{:ok, :reward}
iex> Explorer.Chain.InternalTransaction.Type.cast(:suicide)
{:ok, :suicide}
If `term` is a `String.t`, then it is converted to the corresponding `t:t/0`.
iex> Explorer.Chain.InternalTransaction.Type.cast("call")
{:ok, :call}
iex> Explorer.Chain.InternalTransaction.Type.cast("create")
{:ok, :create}
iex> Explorer.Chain.InternalTransaction.Type.cast("reward")
{:ok, :reward}
iex> Explorer.Chain.InternalTransaction.Type.cast("suicide")
{:ok, :suicide}
Unsupported `String.t` return an `:error`.
iex> Explorer.Chain.InternalTransaction.Type.cast("hard-fork")
:error
"""
@impl Ecto.Type
@spec cast(term()) :: {:ok, t()} | :error
def cast(t) when t in ~w(call create suicide reward)a, do: {:ok, t}
def cast("call"), do: {:ok, :call}
def cast("create"), do: {:ok, :create}
def cast("reward"), do: {:ok, :reward}
def cast("suicide"), do: {:ok, :suicide}
def cast(_), do: :error
@doc """
Dumps the `atom` format to `String.t` format used in the database.
iex> Explorer.Chain.InternalTransaction.Type.dump(:call)
{:ok, "call"}
iex> Explorer.Chain.InternalTransaction.Type.dump(:create)
{:ok, "create"}
iex> Explorer.Chain.InternalTransaction.Type.dump(:reward)
{:ok, "reward"}
iex> Explorer.Chain.InternalTransaction.Type.dump(:suicide)
{:ok, "suicide"}
Other atoms return an error
iex> Explorer.Chain.InternalTransaction.Type.dump(:other)
:error
"""
@impl Ecto.Type
@spec dump(term()) :: {:ok, String.t()} | :error
def dump(:call), do: {:ok, "call"}
def dump(:create), do: {:ok, "create"}
def dump(:reward), do: {:ok, "reward"}
def dump(:suicide), do: {:ok, "suicide"}
def dump(_), do: :error
@doc """
Loads the `t:String.t/0` from the database.
iex> Explorer.Chain.InternalTransaction.Type.load("call")
{:ok, :call}
iex> Explorer.Chain.InternalTransaction.Type.load("create")
{:ok, :create}
iex> Explorer.Chain.InternalTransaction.Type.load("reward")
{:ok, :reward}
iex> Explorer.Chain.InternalTransaction.Type.load("suicide")
{:ok, :suicide}
Other `t:String.t/0` return `:error`
iex> Explorer.Chain.InternalTransaction.Type.load("other")
:error
"""
@impl Ecto.Type
@spec load(term()) :: {:ok, t()} | :error
def load("call"), do: {:ok, :call}
def load("create"), do: {:ok, :create}
def load("reward"), do: {:ok, :reward}
def load("suicide"), do: {:ok, :suicide}
def load(_), do: :error
@doc """
The underlying database type: `:string`
"""
@impl Ecto.Type
@spec type() :: :string
def type, do: :string
end

@ -3,12 +3,41 @@ defmodule Explorer.Chain.Log do
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Address, Receipt} alias Explorer.Chain.{Address, Hash, Receipt, Transaction}
@required_attrs ~w(address_id data index type)a @required_attrs ~w(address_hash data index transaction_hash type)a
@optional_attrs ~w( @optional_attrs ~w(first_topic second_topic third_topic fourth_topic)a
first_topic second_topic third_topic fourth_topic
)a @typedoc """
* `address` - address of contract that generate the event
* `address_hash` - foreign key for `address`
* `data` - non-indexed log parameters.
* `first_topic` - `topics[0]`
* `fourth_topic` - `topics[3]`
* `index` - index of the log entry in all logs for the `receipt` / `transaction`
* `receipt` - receipt for the `transaction` being mined in a block
* `second_topic` - `topics[1]`
* `transaction` - transaction for which `receipt` is
* `transaction_hash` - foreign key for `receipt`. **ALWAYS join through `receipts` and not directly to
`transaction` to ensure that any `t:Explorer.Chain.Transaction.t/0` has a receipt before it has logs in that
receipt.**
* `third_topic` - `topics[2]`
* `type` - type of event
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Truncated.t(),
data: String.t(),
first_topic: String.t(),
fourth_topic: String.t(),
index: non_neg_integer(),
receipt: %Ecto.Association.NotLoaded{} | Receipt.t(),
second_topic: String.t(),
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Hash.Full.t(),
third_topic: String.t(),
type: String.t()
}
schema "logs" do schema "logs" do
field(:data, :string) field(:data, :string)
@ -21,11 +50,46 @@ defmodule Explorer.Chain.Log do
timestamps() timestamps()
belongs_to(:address, Address) belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Truncated)
belongs_to(:receipt, Receipt) belongs_to(:receipt, Receipt, foreign_key: :transaction_hash, references: :transaction_hash, type: Hash.Full)
has_one(:transaction, through: [:receipt, :transaction]) has_one(:transaction, through: [:receipt, :transaction])
end end
@doc """
`address_hash` and `transaction_hash` are converted to `t:Explorer.Chain.Hash.t/0`. The allowed values for `type`
are currently unknown, so it is left as a `t:String.t/0`.
iex> changeset = Explorer.Chain.Log.changeset(
...> %Explorer.Chain.Log{},
...> %{
...> address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22",
...> fourth_topic: nil,
...> index: 0,
...> second_topic: nil,
...> third_topic: nil,
...> transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> type: "mined"
...> }
...> )
iex> changeset.valid?
true
iex> changeset.changes.address_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>>
}
iex> changeset.changes.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>>
}
iex> changeset.changes.type
"mined"
"""
def changeset(%__MODULE__{} = log, attrs \\ %{}) do def changeset(%__MODULE__{} = log, attrs \\ %{}) do
log log
|> cast(attrs, @required_attrs) |> cast(attrs, @required_attrs)
@ -34,4 +98,30 @@ defmodule Explorer.Chain.Log do
|> cast_assoc(:receipt) |> cast_assoc(:receipt)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
end end
@doc """
`address_hash` is always present, so it is always returned in the set.
iex> %Ecto.Changeset{changes: changes, valid?: true} = Explorer.Chain.Log.changeset(
...> %Explorer.Chain.Log{},
...> %{
...> address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> data: "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
...> first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22",
...> fourth_topic: nil,
...> index: 0,
...> second_topic: nil,
...> third_topic: nil,
...> transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
...> type: "mined"
...> }
...> )
iex> address_hash_set = Explorer.Chain.Log.changes_to_address_hash_set(changes)
iex> changes.address_hash in address_hash_set
true
"""
def changes_to_address_hash_set(%{address_hash: address_hash}) do
MapSet.new([address_hash])
end
end end

@ -3,31 +3,53 @@ defmodule Explorer.Chain.Receipt do
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Log, Transaction} alias Explorer.Chain.{Gas, Hash, Log, Transaction}
alias Explorer.Chain.Receipt.Status
@required_attrs ~w(cumulative_gas_used gas_used status index)a @optional_attrs ~w()a
@optional_attrs ~w(transaction_id)a @required_attrs ~w(cumulative_gas_used gas_used status transaction_hash transaction_index)a
@allowed_attrs @optional_attrs ++ @required_attrs
@typedoc """
* `cumulative_gas_used` - the cumulative gas used in `transaction`'s `t:Explorer.Chain.Block.t/0` before
`transaction`'s `index`
* `gas_used` - the gas used for just `transaction`
* `logs` - events that occurred while mining the `transaction`
* `status` - whether the transaction was successfully mined or failed
* `transaction` - the transaction for which this receipt is for
* `transaction_hash` - foreign key for `transaction`
* `transaction_index` - index of `transaction` in its `t:Explorer.Chain.Block.t/0`.
"""
@type t :: %__MODULE__{
cumulative_gas_used: Gas.t(),
gas_used: Gas.t(),
logs: %Ecto.Association.NotLoaded{} | [Log.t()],
status: Status.t(),
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Hash.Full.t(),
transaction_index: non_neg_integer()
}
@primary_key false
schema "receipts" do schema "receipts" do
belongs_to(:transaction, Transaction)
has_many(:logs, Log)
field(:cumulative_gas_used, :decimal) field(:cumulative_gas_used, :decimal)
field(:gas_used, :decimal) field(:gas_used, :decimal)
field(:status, :integer) field(:status, Status)
field(:index, :integer) field(:transaction_index, :integer)
belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)
has_many(:logs, Log, foreign_key: :transaction_hash, references: :transaction_hash)
timestamps() timestamps()
end end
def changeset(%__MODULE__{} = transaction_receipt, attrs \\ %{}) do def changeset(%__MODULE__{} = transaction_receipt, attrs \\ %{}) do
transaction_receipt transaction_receipt
|> cast(attrs, @required_attrs) |> cast(attrs, @allowed_attrs)
|> cast(attrs, @optional_attrs)
|> cast_assoc(:transaction)
|> cast_assoc(:logs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> foreign_key_constraint(:transaction_id) |> foreign_key_constraint(:transaction_hash)
|> unique_constraint(:transaction_id) |> unique_constraint(:transaction_hash)
end end
def null, do: %__MODULE__{} def changes_to_address_hash_set(_), do: MapSet.new()
end end

@ -0,0 +1,109 @@
defmodule Explorer.Chain.Receipt.Status do
@moduledoc """
Whether a transaction succeeded (`:ok`) or failed (`:error`).
Post-[Byzantium](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-609.md) status is `0x1` for success and `0x0`
for failure, but instead of keeping track of just an integer and having to remember if its like C boolean (`0` for
`false`, `1` for `true`) or a Posix exit code, let's represent it as native elixir - `:ok` for success and `:error`
for failure.
"""
@behaviour Ecto.Type
@typedoc """
* `:ok` - transaction succeeded
* `:error` - transaction failed
"""
@type t :: :ok | :error
@doc """
Casts `term` to `t:t/0`
If the `term` is already in `t:t/0`, then it is returned
iex> Explorer.Chain.Receipt.Status.cast(:ok)
{:ok, :ok}
iex> Explorer.Chain.Receipt.Status.cast(:error)
{:ok, :error}
If the `term` is an `non_neg_integer`, then it is converted only if it is `0` or `1`.
iex> Explorer.Chain.Receipt.Status.cast(0)
{:ok, :error}
iex> Explorer.Chain.Receipt.Status.cast(1)
{:ok, :ok}
iex> Explorer.Chain.Receipt.Status.cast(2)
:error
If the `term` is in the quantity format used by `Explorer.JSONRPC`, it is converted only if `0x0` or `0x1`
iex> Explorer.Chain.Receipt.Status.cast("0x0")
{:ok, :error}
iex> Explorer.Chain.Receipt.Status.cast("0x1")
{:ok, :ok}
iex> Explorer.Chain.Receipt.Status.cast("0x2")
:error
"""
@impl Ecto.Type
@spec cast(term()) :: {:ok, t()} | :error
def cast(:error), do: {:ok, :error}
def cast(:ok), do: {:ok, :ok}
def cast(0), do: {:ok, :error}
def cast(1), do: {:ok, :ok}
def cast("0x0"), do: {:ok, :error}
def cast("0x1"), do: {:ok, :ok}
def cast(_), do: :error
@doc """
Dumps the `atom` format to `integer` format used in database.
iex> Explorer.Chain.Receipt.Status.dump(:ok)
{:ok, 1}
iex> Explorer.Chain.Receipt.Status.dump(:error)
{:ok, 0}
If the value hasn't been cast first, it can't be dumped.
iex> Explorer.Chain.Receipt.Status.dump(0)
:error
iex> Explorer.Chain.Receipt.Status.dump(1)
:error
iex> Explorer.Chain.Receipt.Status.dump("0x0")
:error
iex> Explorer.Chain.Receipt.Status.dump("0x1")
:error
"""
@impl Ecto.Type
@spec dump(term()) :: {:ok, 0 | 1} | :error
def dump(:error), do: {:ok, 0}
def dump(:ok), do: {:ok, 1}
def dump(_), do: :error
@doc """
Loads the integer from the database.
Only loads integers `0` and `1`.
iex> Explorer.Chain.Receipt.Status.load(0)
{:ok, :error}
iex> Explorer.Chain.Receipt.Status.load(1)
{:ok, :ok}
iex> Explorer.Chain.Receipt.Status.load(2)
:error
"""
@impl Ecto.Type
@spec load(term()) :: {:ok, t()} | :error
def load(0), do: {:ok, :error}
def load(1), do: {:ok, :ok}
def load(_), do: :error
@doc """
The underlying database type: `:integer`
"""
@impl Ecto.Type
@spec type() :: :integer
def type, do: :integer
end

@ -6,12 +6,10 @@ defmodule Explorer.Chain.Statistics do
import Ecto.Query import Ecto.Query
alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Block, Transaction} alias Explorer.Chain.{Block, Transaction}
alias Explorer.Repo
alias Timex.Duration alias Timex.Duration
# Constants
@average_time_query """ @average_time_query """
SELECT coalesce(avg(difference), interval '0 seconds') SELECT coalesce(avg(difference), interval '0 seconds')
FROM ( FROM (
@ -23,10 +21,9 @@ defmodule Explorer.Chain.Statistics do
""" """
@transaction_count_query """ @transaction_count_query """
SELECT count(transactions.id) SELECT count(transactions.hash)
FROM transactions FROM transactions
JOIN block_transactions ON block_transactions.transaction_id = transactions.id JOIN blocks ON blocks.hash = transactions.block_hash
JOIN blocks ON blocks.id = block_transactions.block_id
WHERE blocks.timestamp > NOW() - interval '1 day' WHERE blocks.timestamp > NOW() - interval '1 day'
""" """
@ -34,7 +31,7 @@ defmodule Explorer.Chain.Statistics do
SELECT COUNT(missing_number) SELECT COUNT(missing_number)
FROM generate_series(0, $1, 1) AS missing_number FROM generate_series(0, $1, 1) AS missing_number
LEFT JOIN blocks ON missing_number = blocks.number LEFT JOIN blocks ON missing_number = blocks.number
WHERE blocks.id IS NULL WHERE blocks.hash IS NULL
""" """
@lag_query """ @lag_query """
@ -48,19 +45,17 @@ defmodule Explorer.Chain.Statistics do
""" """
@block_velocity_query """ @block_velocity_query """
SELECT count(blocks.id) SELECT count(blocks.hash)
FROM blocks FROM blocks
WHERE blocks.inserted_at > NOW() - interval '1 minute' WHERE blocks.inserted_at > NOW() - interval '1 minute'
""" """
@transaction_velocity_query """ @transaction_velocity_query """
SELECT count(transactions.id) SELECT count(transactions.hash)
FROM transactions FROM transactions
WHERE transactions.inserted_at > NOW() - interval '1 minute' WHERE transactions.inserted_at > NOW() - interval '1 minute'
""" """
# Types
@typedoc """ @typedoc """
The number of `t:Explorer.Chain.Block.t/0` mined/validated per minute. The number of `t:Explorer.Chain.Block.t/0` mined/validated per minute.
""" """
@ -72,25 +67,26 @@ defmodule Explorer.Chain.Statistics do
@type transactions_per_minute :: non_neg_integer() @type transactions_per_minute :: non_neg_integer()
@typedoc """ @typedoc """
* `average_time` - the average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0` * `average_time` - the average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0`
* `block_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute * `block_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute
* `blocks` - the last <= 5 `t:Explorer.Chain.Block.t/0` * `blocks` - the last <= 5 `t:Explorer.Chain.Block.t/0`
* `lag` - the average time over the last hour between when the block was mined/validated * `lag` - the average time over the last hour between when the block was mined/validated
(`t:Explorer.Chain.Block.t/0` `timestamp`) and when it was inserted into the databasse (`t:Explorer.Chain.Block.t/0` `timestamp`) and when it was inserted into the databasse
(`t:Explorer.Chain.Block.t/0` `inserted_at`) (`t:Explorer.Chain.Block.t/0` `inserted_at`)
* `number` - the latest `t:Explorer.Chain.Block.t/0` `number` * `number` - the latest `t:Explorer.Chain.Block.t/0` `number`
* `skipped_blocks` - the number of blocks that were mined/validated, but do not exist as `t:Explorer.Chain.Block.t/0` * `skipped_blocks` - the number of blocks that were mined/validated, but do not exist as
* `timestamp` - when the last `t:Explorer.Chain.Block.t/0` was mined/validated `t:Explorer.Chain.Block.t/0`
* `transaction_count` - the number of transactions confirmed in blocks that were mined/validated in the last day * `timestamp` - when the last `t:Explorer.Chain.Block.t/0` was mined/validated
* `transaction_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute * `transaction_count` - the number of transactions confirmed in blocks that were mined/validated in the last day
* `transactions` - the last <= 5 `t:Explorer.Chain.Transaction.t/0` * `transaction_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute
* `transactions` - the last <= 5 `t:Explorer.Chain.Transaction.t/0`
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
average_time: Duration.t(), average_time: Duration.t(),
block_velocity: blocks_per_minute(), block_velocity: blocks_per_minute(),
blocks: [Block.t()], blocks: [Block.t()],
lag: Duration.t(), lag: Duration.t(),
number: Block.number(), number: Block.block_number(),
skipped_blocks: non_neg_integer(), skipped_blocks: non_neg_integer(),
timestamp: :calendar.datetime(), timestamp: :calendar.datetime(),
transaction_count: non_neg_integer(), transaction_count: non_neg_integer(),
@ -98,8 +94,6 @@ defmodule Explorer.Chain.Statistics do
transactions: [Transaction.t()] transactions: [Transaction.t()]
} }
# Struct
defstruct average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0}, defstruct average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
block_velocity: 0, block_velocity: 0,
blocks: [], blocks: [],
@ -111,8 +105,6 @@ defmodule Explorer.Chain.Statistics do
transaction_velocity: 0, transaction_velocity: 0,
transactions: [] transactions: []
# Functions
def fetch do def fetch do
blocks = blocks =
from( from(
@ -131,21 +123,31 @@ defmodule Explorer.Chain.Statistics do
limit: 5 limit: 5
) )
last_block = Block |> Block.latest() |> limit(1) |> Repo.one()
latest_block = last_block || Block.null()
%__MODULE__{ %__MODULE__{
number: latest_block.number,
timestamp: latest_block.timestamp,
average_time: query_duration(@average_time_query), average_time: query_duration(@average_time_query),
transaction_count: query_value(@transaction_count_query),
skipped_blocks: query_value(@skipped_blocks_query, [latest_block.number]),
lag: query_duration(@lag_query),
block_velocity: query_value(@block_velocity_query), block_velocity: query_value(@block_velocity_query),
transaction_velocity: query_value(@transaction_velocity_query),
blocks: Repo.all(blocks), blocks: Repo.all(blocks),
lag: query_duration(@lag_query),
transaction_count: query_value(@transaction_count_query),
transaction_velocity: query_value(@transaction_velocity_query),
transactions: Repo.all(transactions) transactions: Repo.all(transactions)
} }
|> put_max_numbered_block()
end
defp put_max_numbered_block(state) do
case Chain.max_numbered_block() do
{:ok, %Block{number: number, timestamp: timestamp}} ->
%__MODULE__{
state
| number: number,
skipped_blocks: query_value(@skipped_blocks_query, [number]),
timestamp: timestamp
}
{:error, :not_found} ->
state
end
end end
defp query_value(query, args \\ []) do defp query_value(query, args \\ []) do

@ -7,25 +7,25 @@ defmodule Explorer.Chain.Statistics.Server do
@interval 1_000 @interval 1_000
def child_spec(_) do
Supervisor.Spec.worker(__MODULE__, [[refresh: true]])
end
@spec fetch() :: Statistics.t() @spec fetch() :: Statistics.t()
def fetch do def fetch do
case GenServer.whereis(__MODULE__) do GenServer.call(__MODULE__, :fetch)
nil -> Statistics.fetch()
_ -> GenServer.call(__MODULE__, :fetch)
end
end end
def start_link(opts \\ []) do def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__) GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end end
def init(opts) do def init(options) when is_list(options) do
if Keyword.get(opts, :refresh, true) do if Keyword.get(options, :refresh, true) do
{:noreply, chain} = handle_cast({:update, Statistics.fetch()}, %Statistics{}) send(self(), :refresh)
{:ok, chain}
else
{:ok, Statistics.fetch()}
end end
{:ok, %Statistics{}}
end end
def handle_info(:refresh, %Statistics{} = statistics) do def handle_info(:refresh, %Statistics{} = statistics) do

@ -1,20 +0,0 @@
defmodule Explorer.Chain.ToAddress do
@moduledoc false
use Explorer.Schema
alias Explorer.Chain.{Address, Transaction}
@primary_key false
schema "to_addresses" do
belongs_to(:address, Address)
belongs_to(:transaction, Transaction, primary_key: true)
timestamps()
end
def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do
to_address
|> cast(attrs, [:transaction_id, :address_id])
|> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index)
end
end

@ -3,16 +3,11 @@ defmodule Explorer.Chain.Transaction do
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Address, Block, BlockTransaction, Hash, InternalTransaction, Receipt, Wei} alias Ecto.Changeset
alias Explorer.Chain.{Address, Block, Gas, Hash, InternalTransaction, Receipt, Wei}
# Constants @optional_attrs ~w(block_hash from_address_hash index to_address_hash)a
@required_attrs ~w(gas gas_price hash input nonce public_key r s standard_v v value)a
@required_attrs ~w(hash value gas gas_price input nonce public_key r s
standard_v transaction_index v)a
@optional_attrs ~w(to_address_id from_address_id)a
# Types
@typedoc """ @typedoc """
The full public key of the signer of the transaction. The full public key of the signer of the transaction.
@ -76,36 +71,38 @@ defmodule Explorer.Chain.Transaction do
@type wei_per_gas :: Wei.t() @type wei_per_gas :: Wei.t()
@typedoc """ @typedoc """
* `block_transaction` - joins this transaction to its `block` * `block` - the block in which this transaction was mined/validated. `nil` when transaction is pending.
* `block` - the block in which this transaction was mined/validated * `block_hash` - `block` foreign key. `nil` when transaction is pending.
* `from_address` - the source of `value` * `from_address` - the source of `value`
* `from_address_id` - foreign key of `from_address` * `from_address_hash` - foreign key of `from_address`
* `gas` - Gas provided by the sender * `gas` - Gas provided by the sender
* `gas_price` - How much the sender is willing to pay for `gas` * `gas_price` - How much the sender is willing to pay for `gas`
* `hash` - hash of contents of this transaction * `hash` - hash of contents of this transaction
* `input`- data sent along with the transaction * `index` - index of this transaction in `block`. `nil` when transaction is pending.
* `internal_transactions` - transactions (value transfers) created while executing contract used for this transaction * `input`- data sent along with the transaction
* `nonce` - the number of transaction made by the sender prior to this one * `internal_transactions` - transactions (value transfers) created while executing contract used for this
* `public_key` - public key of the signer of the transaction transaction
* `r` - the R field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as * `nonce` - the number of transaction made by the sender prior to this one
the X coordinate of a point R, modulo the curve order n. * `public_key` - public key of the signer of the transaction
* `s` - The S field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as * `r` - the R field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as
the X coordinate of a point R, modulo the curve order n. the X coordinate of a point R, modulo the curve order n.
* `standard_v` - The standardized V field of the signature * `s` - The S field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as
* `to_address` - sink of `value` the X coordinate of a point R, modulo the curve order n.
* `to_address_id` - `to_address` foreign key * `standard_v` - The standardized V field of the signature
* `transaction_index` - index of this transaction in `block` * `to_address` - sink of `value`
* `v` - The V field of the signature. * `to_address_hash` - `to_address` foreign key
* `value` - wei transferred from `from_address` to `to_address` * `v` - The V field of the signature.
* `value` - wei transferred from `from_address` to `to_address`
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
block: %Ecto.Association.NotLoaded{} | Block.t(), block: %Ecto.Association.NotLoaded{} | Block.t() | nil,
block_transaction: %Ecto.Association.NotLoaded{} | BlockTransaction.t(), block_hash: Hash.t() | nil,
from_address: %Ecto.Association.NotLoaded{} | Address.t(), from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_id: non_neg_integer(), from_address_hash: Hash.Truncated.t(),
gas: Gas.t(), gas: Gas.t(),
gas_price: wei_per_gas, gas_price: wei_per_gas,
hash: Hash.t(), hash: Hash.t(),
index: non_neg_integer() | nil,
input: String.t(), input: String.t(),
internal_transactions: %Ecto.Association.NotLoaded{} | [InternalTransaction.t()], internal_transactions: %Ecto.Association.NotLoaded{} | [InternalTransaction.t()],
nonce: non_neg_integer(), nonce: non_neg_integer(),
@ -115,47 +112,239 @@ defmodule Explorer.Chain.Transaction do
s: s(), s: s(),
standard_v: standard_v(), standard_v: standard_v(),
to_address: %Ecto.Association.NotLoaded{} | Address.t(), to_address: %Ecto.Association.NotLoaded{} | Address.t(),
to_address_id: non_neg_integer(), to_address_hash: Hash.Truncated.t(),
transaction_index: non_neg_integer(),
v: v(), v: v(),
value: Wei.t() value: Wei.t()
} }
# Schema @primary_key {:hash, Hash.Full, autogenerate: false}
schema "transactions" do schema "transactions" do
field(:gas, :decimal) field(:gas, :decimal)
field(:gas_price, Wei) field(:gas_price, Wei)
field(:hash, :string) field(:index, :integer)
field(:input, :string) field(:input, :string)
field(:nonce, :integer) field(:nonce, :integer)
field(:public_key, :string) field(:public_key, :string)
field(:r, :string) field(:r, :string)
field(:s, :string) field(:s, :string)
field(:standard_v, :string) field(:standard_v, :string)
field(:transaction_index, :string)
field(:v, :string) field(:v, :string)
field(:value, Wei) field(:value, Wei)
timestamps() timestamps()
has_one(:block_transaction, BlockTransaction) belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full)
has_one(:block, through: [:block_transaction, :block])
belongs_to(:from_address, Address) belongs_to(
has_many(:internal_transactions, InternalTransaction) :from_address,
has_one(:receipt, Receipt) Address,
belongs_to(:to_address, Address) foreign_key: :from_address_hash,
references: :hash,
type: Hash.Truncated
)
has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash)
has_one(:receipt, Receipt, foreign_key: :transaction_hash)
belongs_to(
:to_address,
Address,
foreign_key: :to_address_hash,
references: :hash,
type: Hash.Truncated
)
end end
@doc false @doc """
A pending transaction has neither `block_hash` nor an `index`
iex> changeset = Explorer.Chain.Transaction.changeset(
...> %Transaction{},
...> %{
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> v: "0x8d",
...> value: 0
...> }
...> )
iex> changeset.valid?
true
A pending transaction can't have an `index`
iex> changeset = Explorer.Chain.Transaction.changeset(
...> %Transaction{},
...> %{
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> v: "0x8d",
...> value: 0
...> }
...> )
iex> changeset.valid?
false
iex> changeset.errors
[index: {"can't be set when the transaction is pending", []}]
A collated transaction has a `block_hash` for the block in which it was collated.
iex> changeset = Explorer.Chain.Transaction.changeset(
...> %Transaction{},
...> %{
...> block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> v: "0x8d",
...> value: 0
...> }
...> )
iex> changeset.valid?
true
A collated transaction MUST have an `index` so its position in the `block` is known.
iex> changeset = Explorer.Chain.Transaction.changeset(
...> %Transaction{},
...> %{
...> block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> v: "0x8d",
...> value: 0
...> }
...> )
iex> changeset.valid?
false
iex> changeset.errors
[index: {"can't be blank when transaction is collated into a block", []}]
"""
def changeset(%__MODULE__{} = transaction, attrs \\ %{}) do def changeset(%__MODULE__{} = transaction, attrs \\ %{}) do
transaction transaction
|> cast(attrs, @required_attrs ++ @optional_attrs) |> cast(attrs, @required_attrs ++ @optional_attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> foreign_key_constraint(:block_id) |> validate_collated()
|> update_change(:hash, &String.downcase/1) |> check_constraint(
:index,
message: "cannot be set when block_hash is nil and must be set when block_hash is not nil",
name: :indexed
)
|> foreign_key_constraint(:block_hash)
|> unique_constraint(:hash) |> unique_constraint(:hash)
end end
def null, do: %__MODULE__{} @doc """
A transaction transfering `value` between two addresses has a `from_address_hash` and `to_address_hash`
iex> %Ecto.Changeset{changes: changes, valid?: true} = Explorer.Chain.Transaction.changeset(
...> %Transaction{},
...> %{
...> block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> v: "0x8d",
...> value: 0
...> }
...> )
iex> address_hash_set = Explorer.Chain.Transaction.changes_to_address_hash_set(changes)
iex> changes.from_address_hash in address_hash_set
true
iex> changes.to_address_hash in address_hash_set
true
A contract creation transaction does not have a `to_address_hash`, so the `t:MapSet.t/0` only contains the
`from_address_hash`.
iex> %Ecto.Changeset{changes: changes, valid?: true} = Explorer.Chain.Transaction.changeset(
...> %Transaction{},
...> %{
...> block_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> gas: 4700000,
...> gas_price: 100000000000,
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 0,
...> input: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> nonce: 0,
...> public_key: "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> r: "0xAD3733DF250C87556335FFE46C23E34DBAFFDE93097EF92F52C88632A40F0C75",
...> s: "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> standard_v: "0x0",
...> to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
...> v: "0x8d",
...> value: 0
...> }
...> )
iex> Explorer.Chain.Transaction.changes_to_address_hash_set(changes)
MapSet.new([
%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>>
}
])
"""
def changes_to_address_hash_set(changes) do
Enum.reduce(~w(from_address_hash to_address_hash)a, MapSet.new(), fn field, acc ->
case Map.get(changes, field) do
nil -> acc
value -> MapSet.put(acc, value)
end
end)
end
defp validate_collated(%Changeset{} = changeset) do
case {Changeset.get_field(changeset, :block_hash), Changeset.get_field(changeset, :index)} do
{nil, nil} ->
changeset
{_block_hash, nil} ->
Changeset.add_error(changeset, :index, "can't be blank when transaction is collated into a block")
{nil, _index} ->
Changeset.add_error(changeset, :index, "can't be set when the transaction is pending")
_ ->
changeset
end
end
end end

@ -89,7 +89,7 @@ defmodule Explorer.Chain.Wei do
@typedoc """ @typedoc """
Short for giga-wei Short for giga-wei
* 10<sup>9</sup> wei is one gwei 10<sup>9</sup> wei is 1 gwei.
""" """
@type gwei :: Decimal.t() @type gwei :: Decimal.t()
@ -108,13 +108,9 @@ defmodule Explorer.Chain.Wei do
value: Decimal.t() value: Decimal.t()
} }
# Constants
@wei_per_ether Decimal.new(1_000_000_000_000_000_000) @wei_per_ether Decimal.new(1_000_000_000_000_000_000)
@wei_per_gwei Decimal.new(1_000_000_000) @wei_per_gwei Decimal.new(1_000_000_000)
## Functions
@doc """ @doc """
Converts `Decimal` representations of various wei denominations (wei, Gwei, ether) to Converts `Decimal` representations of various wei denominations (wei, Gwei, ether) to
a wei base unit. a wei base unit.
@ -148,7 +144,7 @@ defmodule Explorer.Chain.Wei do
%__MODULE__{value: Decimal.mult(gwei, @wei_per_gwei)} %__MODULE__{value: Decimal.mult(gwei, @wei_per_gwei)}
end end
@spec from(t(), :wei) :: t() @spec from(wei(), :wei) :: t()
def from(%Decimal{} = wei, :wei) do def from(%Decimal{} = wei, :wei) do
%__MODULE__{value: wei} %__MODULE__{value: wei}
end end

@ -1,19 +0,0 @@
defmodule Explorer.Ethereum do
@client Application.get_env(:explorer, :ethereum)[:backend]
defmodule API do
@moduledoc false
@callback download_balance(String.t()) :: String.t()
end
defdelegate download_balance(hash), to: @client
def decode_integer_field(hex) do
{"0x", base_16} = String.split_at(hex, 2)
String.to_integer(base_16, 16)
end
def decode_time_field(field) do
field |> decode_integer_field() |> Timex.from_unix()
end
end

@ -1,14 +0,0 @@
defmodule Explorer.Ethereum.Live do
@moduledoc """
An implementation for Ethereum that uses the actual node.
"""
@behaviour Explorer.Ethereum.API
import Ethereumex.HttpClient, only: [eth_get_balance: 1]
def download_balance(hash) do
{:ok, result} = eth_get_balance(hash)
result
end
end

@ -1,9 +0,0 @@
defmodule Explorer.Ethereum.Test do
@moduledoc """
An interface for the Ethereum node that does not hit the network
"""
@behaviour Explorer.Ethereum.API
def download_balance(_hash) do
"0x15d231fca629c7c0"
end
end

@ -1,14 +0,0 @@
defmodule Explorer.EthereumexExtensions do
@moduledoc """
Downloads the trace for a Transaction from a node.
"""
alias Ethereumex.HttpClient
@dialyzer {:nowarn_function, trace_transaction: 1}
def trace_transaction(hash) do
params = [hash, ["trace"]]
{:ok, trace} = HttpClient.request("trace_replayTransaction", params, [])
trace
end
end

@ -14,8 +14,6 @@ defmodule Explorer.ExchangeRates do
@interval :timer.minutes(5) @interval :timer.minutes(5)
@table_name :exchange_rates @table_name :exchange_rates
## GenServer functions
@impl GenServer @impl GenServer
def handle_info(:update, state) do def handle_info(:update, state) do
Logger.debug(fn -> "Updating cached exchange rates" end) Logger.debug(fn -> "Updating cached exchange rates" end)
@ -80,8 +78,6 @@ defmodule Explorer.ExchangeRates do
GenServer.start_link(__MODULE__, opts, name: __MODULE__) GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end end
## Public functions
@doc """ @doc """
Lists exchange rates for the tracked tickers. Lists exchange rates for the tracked tickers.
""" """
@ -93,7 +89,7 @@ defmodule Explorer.ExchangeRates do
@doc """ @doc """
Returns a specific rate from the tracked tickers by symbol Returns a specific rate from the tracked tickers by symbol
""" """
@spec lookup(String.t()) :: Token.t() @spec lookup(String.t()) :: Token.t() | nil
def lookup(symbol) do def lookup(symbol) do
if store() == :ets do if store() == :ets do
case :ets.lookup(table_name(), symbol) do case :ets.lookup(table_name(), symbol) do
@ -103,14 +99,10 @@ defmodule Explorer.ExchangeRates do
end end
end end
## Undocumented public functions
@doc false @doc false
@spec table_name() :: atom() @spec table_name() :: atom()
def table_name, do: @table_name def table_name, do: @table_name
## Private functions
@spec config(atom()) :: term @spec config(atom()) :: term
defp config(key) do defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key] Application.get_env(:explorer, __MODULE__, [])[key]

@ -6,15 +6,15 @@ defmodule Explorer.ExchangeRates.Token do
@typedoc """ @typedoc """
Represents an exchange rate for a given token. Represents an exchange rate for a given token.
* `:available_supply` - Available supply of a token * `:available_supply` - Available supply of a token
* `:btc_value` - The Bitcoin value of the currency * `:btc_value` - The Bitcoin value of the currency
* `:id` - ID of a currency * `:id` - ID of a currency
* `:last_updated` - Timestamp of when the value was last updated * `:last_updated` - Timestamp of when the value was last updated
* `:market_cap_usd` - Market capitalization of the currency * `:market_cap_usd` - Market capitalization of the currency
* `:name` - Human-readable name of a ticker * `:name` - Human-readable name of a ticker
* `:symbol` - Trading symbol used to represent a currency * `:symbol` - Trading symbol used to represent a currency
* `:usd_value` - The USD value of the currency * `:usd_value` - The USD value of the currency
* `:volume_24h_usd` - The volume from the last 24 hours in USD * `:volume_24h_usd` - The volume from the last 24 hours in USD
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
available_supply: Decimal.t(), available_supply: Decimal.t(),

@ -1,5 +0,0 @@
defmodule Explorer.ExqNodeIdentifier do
@behaviour Exq.NodeIdentifier.Behaviour
@moduledoc "Configure Exq with the current dyno name"
def node_id, do: System.get_env("DYNO")
end

@ -1,17 +0,0 @@
defmodule Explorer.BalanceImporter do
@moduledoc "Imports a balance for a given address."
alias Explorer.{Chain, Ethereum}
def import(hash) do
encoded_balance = Ethereum.download_balance(hash)
persist_balance(hash, encoded_balance)
end
defp persist_balance(hash, encoded_balance) when is_binary(hash) do
decoded_balance = Ethereum.decode_integer_field(encoded_balance)
Chain.update_balance(hash, decoded_balance)
end
end

@ -1,81 +0,0 @@
defmodule Explorer.BlockImporter do
@moduledoc "Imports a block."
import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_block_by_number: 2]
alias Explorer.{BlockImporter, Ethereum}
alias Explorer.Chain.Block
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Workers.ImportTransaction
def import(raw_block) when is_map(raw_block) do
changes = extract_block(raw_block)
block = changes.hash |> find()
if is_nil(block.id), do: block |> Block.changeset(changes) |> Repo.insert()
Enum.map(raw_block["transactions"], &ImportTransaction.perform/1)
end
@dialyzer {:nowarn_function, import: 1}
def import("pending") do
raw_block = download_block("pending")
Enum.map(raw_block["transactions"], &ImportTransaction.perform_later/1)
end
@dialyzer {:nowarn_function, import: 1}
def import(block_number) do
block_number |> download_block() |> BlockImporter.import()
end
def find(hash) do
query =
from(
b in Block,
where: fragment("lower(?)", b.hash) == ^String.downcase(hash),
limit: 1
)
query |> Repo.one() || %Block{}
end
@dialyzer {:nowarn_function, download_block: 1}
def download_block(block_number) do
{:ok, block} =
block_number
|> encode_number()
|> eth_get_block_by_number(true)
block
end
def extract_block(raw_block) do
%{
hash: raw_block["hash"],
number: raw_block["number"] |> Ethereum.decode_integer_field(),
gas_used: raw_block["gasUsed"] |> Ethereum.decode_integer_field(),
timestamp: raw_block["timestamp"] |> Ethereum.decode_time_field(),
parent_hash: raw_block["parentHash"],
miner: raw_block["miner"],
difficulty: raw_block["difficulty"] |> Ethereum.decode_integer_field(),
total_difficulty: raw_block["totalDifficulty"] |> Ethereum.decode_integer_field(),
size: raw_block["size"] |> Ethereum.decode_integer_field(),
gas_limit: raw_block["gasLimit"] |> Ethereum.decode_integer_field(),
nonce: raw_block["nonce"] || "0"
}
end
defp encode_number("latest"), do: "latest"
defp encode_number("earliest"), do: "earliest"
defp encode_number("pending"), do: "pending"
defp encode_number("0x" <> number) when is_binary(number), do: number
defp encode_number(number) when is_binary(number) do
number
|> String.to_integer()
|> encode_number()
end
defp encode_number(number), do: "0x" <> Integer.to_string(number, 16)
end

@ -1,80 +0,0 @@
defmodule Explorer.InternalTransactionImporter do
@moduledoc "Imports a transaction's internal transactions given its hash."
import Ecto.Query
alias Explorer.{Chain, Ethereum, EthereumexExtensions, Repo}
alias Explorer.Chain.{InternalTransaction, Transaction}
@dialyzer {:nowarn_function, import: 1}
def import(hash) do
transaction = find_transaction(hash)
hash
|> download_trace
|> extract_attrs
|> persist_internal_transactions(transaction)
end
@dialyzer {:nowarn_function, download_trace: 1}
defp download_trace(hash) do
EthereumexExtensions.trace_transaction(hash)
end
defp find_transaction(hash) do
query =
from(
t in Transaction,
where: fragment("lower(?)", t.hash) == ^String.downcase(hash),
limit: 1
)
Repo.one!(query)
end
@dialyzer {:nowarn_function, extract_attrs: 1}
defp extract_attrs(attrs) do
trace = attrs["trace"]
trace |> Enum.with_index() |> Enum.map(&extract_trace/1)
end
def extract_trace({trace, index}) do
%{
index: index,
call_type: trace["action"]["callType"] || trace["type"],
to_address_id: trace |> to_address() |> address_id(),
from_address_id: trace |> from_address() |> address_id(),
trace_address: trace["traceAddress"],
value: trace["action"]["value"] |> Ethereum.decode_integer_field(),
gas: trace["action"]["gas"] |> Ethereum.decode_integer_field(),
gas_used: trace["result"]["gasUsed"] |> Ethereum.decode_integer_field(),
input: trace["action"]["input"],
output: trace["result"]["output"]
}
end
defp to_address(%{"action" => %{"to" => address}})
when not is_nil(address),
do: address
defp to_address(%{"result" => %{"address" => address}}), do: address
defp from_address(%{"action" => %{"from" => address}}), do: address
@dialyzer {:nowarn_function, persist_internal_transactions: 2}
defp persist_internal_transactions(traces, transaction) do
Enum.map(traces, fn trace ->
trace = Map.merge(trace, %{transaction_id: transaction.id})
%InternalTransaction{}
|> InternalTransaction.changeset(trace)
|> Repo.insert()
end)
end
defp address_id(hash) do
{:ok, address} = Chain.ensure_hash_address(hash)
address.id
end
end

@ -1,79 +0,0 @@
defmodule Explorer.ReceiptImporter do
@moduledoc "Imports a transaction receipt given a transaction hash."
import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1]
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Receipt, Transaction}
def import(hash) do
transaction = hash |> find_transaction()
hash
|> download_receipt()
|> extract_receipt()
|> Map.put(:transaction_id, transaction.id)
|> save_receipt()
end
@dialyzer {:nowarn_function, download_receipt: 1}
defp download_receipt(hash) do
{:ok, receipt} = eth_get_transaction_receipt(hash)
receipt || %{}
end
defp find_transaction(hash) do
query =
from(
transaction in Transaction,
left_join: receipt in assoc(transaction, :receipt),
where: fragment("lower(?)", transaction.hash) == ^hash,
where: is_nil(receipt.id),
limit: 1
)
Repo.one(query) || Transaction.null()
end
defp save_receipt(receipt) do
unless is_nil(receipt.transaction_id) do
%Receipt{}
|> Receipt.changeset(receipt)
|> Repo.insert()
end
end
defp extract_receipt(receipt) do
logs = receipt["logs"] || []
%{
index: receipt["transactionIndex"] |> decode_integer_field(),
cumulative_gas_used: receipt["cumulativeGasUsed"] |> decode_integer_field(),
gas_used: receipt["gasUsed"] |> decode_integer_field(),
status: receipt["status"] |> decode_integer_field(),
logs: logs |> Enum.map(&extract_log/1)
}
end
defp extract_log(log) do
{:ok, address} = Chain.ensure_hash_address(log["address"])
%{
address_id: address.id,
index: log["logIndex"] |> decode_integer_field(),
data: log["data"],
type: log["type"],
first_topic: log["topics"] |> Enum.at(0),
second_topic: log["topics"] |> Enum.at(1),
third_topic: log["topics"] |> Enum.at(2),
fourth_topic: log["topics"] |> Enum.at(3)
}
end
defp decode_integer_field("0x" <> hex) when is_binary(hex) do
String.to_integer(hex, 16)
end
defp decode_integer_field(field), do: field
end

@ -1,142 +0,0 @@
defmodule Explorer.TransactionImporter do
@moduledoc "Imports a transaction given a unique hash."
import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1]
alias Explorer.{Chain, Ethereum, Repo, BalanceImporter}
alias Explorer.Chain.{Block, BlockTransaction, Transaction}
def import(hash) when is_binary(hash) do
hash |> download_transaction() |> persist_transaction()
end
def import(raw_transaction) when is_map(raw_transaction) do
persist_transaction(raw_transaction)
end
def persist_transaction(raw_transaction) do
found_transaction = raw_transaction["hash"] |> find()
transaction =
case is_nil(found_transaction.id) do
false ->
found_transaction
true ->
to_address =
raw_transaction
|> to_address()
|> fetch_address()
from_address =
raw_transaction
|> from_address()
|> fetch_address()
changes =
raw_transaction
|> extract_attrs()
|> Map.put(:to_address_id, to_address.id)
|> Map.put(:from_address_id, from_address.id)
found_transaction |> Transaction.changeset(changes) |> Repo.insert!()
end
transaction
|> create_block_transaction(raw_transaction["blockHash"])
refresh_account_balances(raw_transaction)
transaction
end
def find(hash) do
query =
from(
t in Transaction,
where: fragment("lower(?)", t.hash) == ^String.downcase(hash),
limit: 1
)
query |> Repo.one() || %Transaction{}
end
def download_transaction(hash) do
{:ok, payload} = eth_get_transaction_by_hash(hash)
payload
end
def extract_attrs(raw_transaction) do
%{
hash: raw_transaction["hash"],
value: raw_transaction["value"] |> Ethereum.decode_integer_field(),
gas: raw_transaction["gas"] |> Ethereum.decode_integer_field(),
gas_price: raw_transaction["gasPrice"] |> Ethereum.decode_integer_field(),
input: raw_transaction["input"],
nonce: raw_transaction["nonce"] |> Ethereum.decode_integer_field(),
public_key: raw_transaction["publicKey"],
r: raw_transaction["r"],
s: raw_transaction["s"],
standard_v: raw_transaction["standardV"],
transaction_index: raw_transaction["transactionIndex"],
v: raw_transaction["v"]
}
end
def create_block_transaction(transaction, hash) do
query =
from(
t in Block,
where: fragment("lower(?)", t.hash) == ^String.downcase(hash),
limit: 1
)
block = query |> Repo.one()
if block do
changes = %{block_id: block.id, transaction_id: transaction.id}
case Repo.get_by(BlockTransaction, transaction_id: transaction.id) do
nil ->
%BlockTransaction{}
|> BlockTransaction.changeset(changes)
|> Repo.insert()
block_transaction ->
block_transaction
|> BlockTransaction.changeset(%{block_id: block.id})
|> Repo.update()
end
end
transaction
end
def to_address(%{"to" => to}) when not is_nil(to), do: to
def to_address(%{"creates" => creates}) when not is_nil(creates), do: creates
def to_address(hash) when is_bitstring(hash), do: hash
def from_address(%{"from" => from}), do: from
def from_address(hash) when is_bitstring(hash), do: hash
def fetch_address(hash) when is_bitstring(hash) do
{:ok, address} = Chain.ensure_hash_address(hash)
address
end
defp refresh_account_balances(raw_transaction) do
raw_transaction
|> to_address()
|> update_balance()
raw_transaction
|> from_address()
|> update_balance()
end
defp update_balance(address_hash) do
BalanceImporter.import(address_hash)
end
end

@ -0,0 +1,55 @@
defmodule Explorer.Indexer do
@moduledoc """
Indexes an Ethereum-based chain using JSONRPC.
"""
alias Explorer.Chain
@doc """
The maximum `t:Explorer.Chain.Block.t/0` `number` that was indexed
If blocks are skipped and inserted out of number order, the max number is still returned
iex> insert(:block, number: 2)
iex> insert(:block, number: 1)
iex> Explorer.Indexer.max_block_number()
2
If there are no blocks, `0` is returned to indicate to index from genesis block.
iex> Explorer.Indexer.max_block_number()
0
"""
def max_block_number do
case Chain.max_block_number() do
{:ok, number} -> number
{:error, :not_found} -> 0
end
end
@doc """
The next `t:Explorer.Chain.Block.t/0` `number` that needs to be indexed (excluding skipped blocks)
When there are no blocks the next block is the 0th block
iex> Explorer.Indexer.max_block_number()
0
iex> Explorer.Indexer.next_block_number()
0
When there is a block, it is the successive block number
iex> insert(:block, number: 2)
iex> insert(:block, number: 1)
iex> Explorer.Indexer.next_block_number()
3
"""
def next_block_number do
case max_block_number() do
0 -> 0
num -> num + 1
end
end
end

@ -0,0 +1,157 @@
defmodule Explorer.Indexer.AddressFetcher do
@moduledoc """
Fetches and indexes `t:Explorer.Chain.Address.t/0` balances.
"""
use GenServer
require Logger
alias EthereumJSONRPC
alias Explorer.Chain
alias Explorer.Chain.{Address, Hash}
@fetch_interval :timer.seconds(3)
@max_batch_size 100
@max_concurrency 2
def async_fetch_balances(address_hashes) do
GenServer.cast(__MODULE__, {:buffer_addresses, address_hashes})
end
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(opts) do
opts = Keyword.merge(Application.fetch_env!(:explorer, :indexer), opts)
send(self(), :fetch_unfetched_addresses)
state = %{
debug_logs: Keyword.get(opts, :debug_logs, false),
flush_timer: nil,
fetch_interval: Keyword.get(opts, :fetch_interval, @fetch_interval),
max_batch_size: Keyword.get(opts, :max_batch_size, @max_batch_size),
buffer: :queue.new(),
tasks: %{}
}
{:ok, state}
end
def handle_info(:fetch_unfetched_addresses, state) do
{:noreply, stream_unfetched_addresses(state)}
end
def handle_info(:flush, state) do
{:noreply, state |> fetch_next_batch([]) |> schedule_next_buffer_flush()}
end
def handle_info({:async_fetch, hashes}, state) do
{:noreply, fetch_next_batch(state, hashes)}
end
def handle_info({ref, {:fetched_balances, results}}, state) do
:ok = Chain.update_balances(results)
{:noreply, drop_task(state, ref)}
end
def handle_info({:DOWN, _ref, :process, _pid, :normal}, state) do
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
batch = Map.fetch!(state.tasks, ref)
new_state =
state
|> drop_task(ref)
|> buffer_addresses(batch)
{:noreply, new_state}
end
def handle_cast({:buffer_addresses, address_hashes}, state) do
string_hashes = for hash <- address_hashes, do: Hash.to_string(hash)
{:noreply, buffer_addresses(state, string_hashes)}
end
defp drop_task(state, ref) do
schedule_async_fetch([])
%{state | tasks: Map.delete(state.tasks, ref)}
end
defp buffer_addresses(state, string_hashes) do
%{state | buffer: :queue.join(state.buffer, :queue.from_list(string_hashes))}
end
defp stream_unfetched_addresses(state) do
state.buffer
|> Chain.stream_unfetched_addresses(fn %Address{hash: hash}, batch ->
batch = :queue.in(Hash.to_string(hash), batch)
if :queue.len(batch) >= state.max_batch_size do
schedule_async_fetch(:queue.to_list(batch))
:queue.new()
else
batch
end
end)
|> fetch_remaining()
schedule_next_buffer_flush(state)
end
defp fetch_remaining({:ok, batch}) do
if :queue.len(batch) > 0 do
schedule_async_fetch(:queue.to_list(batch))
end
:ok
end
defp do_fetch_addresses(address_hashes) do
EthereumJSONRPC.fetch_balances_by_hash(address_hashes)
end
defp take_batch(queue) do
{hashes, remaining_queue} =
Enum.reduce_while(1..@max_batch_size, {[], queue}, fn _, {hashes, queue_acc} ->
case :queue.out(queue_acc) do
{{:value, hash}, new_queue} -> {:cont, {[hash | hashes], new_queue}}
{:empty, new_queue} -> {:halt, {hashes, new_queue}}
end
end)
{Enum.reverse(hashes), remaining_queue}
end
defp schedule_async_fetch(hashes, after_ms \\ 0) do
Process.send_after(self(), {:async_fetch, hashes}, after_ms)
end
defp schedule_next_buffer_flush(state) do
timer = Process.send_after(self(), :flush, state.fetch_interval)
%{state | flush_timer: timer}
end
defp fetch_next_batch(state, hashes) do
state = buffer_addresses(state, hashes)
if Enum.count(state.tasks) < @max_concurrency and :queue.len(state.buffer) > 0 do
{batch, new_queue} = take_batch(state.buffer)
task =
Task.Supervisor.async_nolink(Explorer.Indexer.TaskSupervisor, fn ->
debug(state, fn -> "fetching #{Enum.count(batch)} balances" end)
{:ok, balances} = do_fetch_addresses(batch)
{:fetched_balances, balances}
end)
%{state | tasks: Map.put(state.tasks, task.ref, batch), buffer: new_queue}
else
buffer_addresses(state, hashes)
end
end
defp debug(%{debug_logs: true}, func), do: Logger.debug(func)
defp debug(%{debug_logs: false}, _func), do: :noop
end

@ -0,0 +1,305 @@
defmodule Explorer.Indexer.BlockFetcher do
@moduledoc """
Fetches and indexes block ranges from gensis to realtime.
"""
use GenServer
require Logger
alias EthereumJSONRPC
alias EthereumJSONRPC.Transactions
alias Explorer.{Chain, Indexer}
alias Explorer.Indexer.{AddressFetcher, Sequence}
# dialyzer thinks that Logger.debug functions always have no_local_return
@dialyzer {:nowarn_function, import_range: 3}
# These are all the *default* values for options.
# DO NOT use them directly in the code. Get options from `state`.
@debug_logs false
@blocks_batch_size 10
@blocks_concurrency 10
@internal_transactions_batch_size 50
@internal_transactions_concurrency 8
# milliseconds
@block_rate 5_000
@receipts_batch_size 250
@receipts_concurrency 20
@doc """
Starts the server.
## Options
Default options are pulled from application config under the
`:explorer, :indexer` keyspace. The follow options can be overridden:
* `:debug_logs` - When `true` logs verbose index progress. Defaults `#{@debug_logs}`.
* `:blocks_batch_size` - The number of blocks to request in one call to the JSONRPC. Defaults to
`#{@blocks_batch_size}`. Block requests also include the transactions for those blocks. *These transactions
are not paginated.*
* `:blocks_concurrency` - The number of concurrent requests of `:blocks_batch_size` to allow against the JSONRPC.
Defaults to #{@blocks_concurrency}. So upto `blocks_concurrency * block_batch_size` (defaults to
`#{@blocks_concurrency * @blocks_batch_size}`) blocks can be requested from the JSONRPC at once over all
connections.
* `:block_rate` - The millisecond rate new blocks are published at. Defaults to `#{@block_rate}` milliseconds.
* `:internal transactions_batch_size` - The number of transaction hashes to request internal transactions for
in one call to the JSONRPC. Defaults to `#{@internal_transactions_batch_size}`.
* `:internal transactions_concurrency` - The number of concurrent requests of `:internal transactions_batch_size` to
allow against the JSONRPC **for each block range**. Defaults to `#{@internal_transactions_concurrency}`. So upto
`block_concurrency * internal_transactions_batch_size * internal transactions_concurrency` (defaults to
`#{@blocks_concurrency * @internal_transactions_concurrency * @internal_transactions_batch_size}`) transactions
can be requesting their internal transactions can be requested from the JSONRPC at once over all connections.
*The internal transactions for individual transactions cannot be paginated, so the total number of internal
transactions that could be produced is unknown.*
* `:receipts_batch_size` - The number of receipts to request in one call to the JSONRPC. Defaults to
`#{@receipts_batch_size}`. Receipt requests also include the logs for when the transaction was collated into the
block. *These logs are not paginated.*
* `:receipts_concurrency` - The number of concurrent requests of `:receipts_batch_size` to allow against the JSONRPC
**for each block range**. Defaults to `#{@receipts_concurrency}`. So upto
`block_concurrency * receipts_batch_size * receipts_concurrency` (defaults to
`#{@blocks_concurrency * @receipts_concurrency * @receipts_batch_size}`) receipts can be requested from the
JSONRPC at once over all connections. *Each transaction only has one receipt.*
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl GenServer
def init(opts) do
opts = Keyword.merge(Application.fetch_env!(:explorer, :indexer), opts)
:timer.send_interval(15_000, self(), :debug_count)
state = %{
genesis_task: nil,
realtime_task: nil,
debug_logs: Keyword.get(opts, :debug_logs, @debug_logs),
realtime_interval: (opts[:block_rate] || @block_rate) * 2,
blocks_batch_size: Keyword.get(opts, :blocks_batch_size, @blocks_batch_size),
blocks_concurrency: Keyword.get(opts, :blocks_concurrency, @blocks_concurrency),
internal_transactions_batch_size:
Keyword.get(opts, :internal_transactions_batch_size, @internal_transactions_batch_size),
internal_transactions_concurrency:
Keyword.get(opts, :internal_transactions_concurrency, @internal_transactions_concurrency),
receipts_batch_size: Keyword.get(opts, :receipts_batch_size, @receipts_batch_size),
receipts_concurrency: Keyword.get(opts, :receipts_concurrency, @receipts_concurrency)
}
{:ok, schedule_next_catchup_index(state)}
end
@impl GenServer
def handle_info(:catchup_index, %{} = state) do
{:ok, genesis_task, _ref} = monitor_task(fn -> genesis_task(state) end)
{:noreply, %{state | genesis_task: genesis_task}}
end
def handle_info(:realtime_index, %{} = state) do
{:ok, realtime_task, _ref} = monitor_task(fn -> realtime_task(state) end)
{:noreply, %{state | realtime_task: realtime_task}}
end
def handle_info({:DOWN, _ref, :process, pid, :normal}, %{realtime_task: pid} = state) do
{:noreply, schedule_next_realtime_fetch(%{state | realtime_task: nil})}
end
def handle_info({:DOWN, _ref, :process, pid, _reason}, %{realtime_task: pid} = state) do
Logger.error(fn -> "realtime index stream exited. Restarting" end)
{:noreply, schedule_next_realtime_fetch(%{state | realtime_task: nil})}
end
def handle_info({:DOWN, _ref, :process, pid, :normal}, %{genesis_task: pid} = state) do
Logger.info(fn -> "Finished index from genesis. Transitioning to realtime index." end)
{:noreply, schedule_next_realtime_fetch(%{state | genesis_task: nil})}
end
def handle_info({:DOWN, _ref, :process, pid, _reason}, %{genesis_task: pid} = state) do
Logger.error(fn -> "gensis index stream exited. Restarting" end)
{:noreply, schedule_next_catchup_index(%{state | genesis_task: nil})}
end
def handle_info(:debug_count, %{} = state) do
debug(state, fn ->
"""
================================
persisted counts
================================
blocks: #{Chain.block_count()}
internal transactions: #{Chain.internal_transaction_count()}
receipts: #{Chain.receipt_count()}
logs: #{Chain.log_count()}
addresses: #{Chain.address_count()}
"""
end)
{:noreply, state}
end
defp cap_seq(seq, :end_of_chain, {_block_start, _block_end}, _state) do
:ok = Sequence.cap(seq)
end
defp cap_seq(_seq, :more, {block_start, block_end}, %{} = state) do
debug(state, fn -> "got blocks #{block_start} - #{block_end}" end)
:ok
end
defp fetch_internal_transactions(_state, []), do: {:ok, []}
defp fetch_internal_transactions(%{} = state, hashes) do
debug(state, fn -> "fetching internal transactions for #{length(hashes)} transactions" end)
stream_opts = [max_concurrency: state.internal_transactions_concurrency, timeout: :infinity]
hashes
|> Enum.chunk_every(state.internal_transactions_batch_size)
|> Task.async_stream(&EthereumJSONRPC.fetch_internal_transactions(&1), stream_opts)
|> Enum.reduce_while({:ok, []}, fn
{:ok, {:ok, internal_transactions}}, {:ok, acc} -> {:cont, {:ok, acc ++ internal_transactions}}
{:ok, {:error, reason}}, {:ok, _acc} -> {:halt, {:error, reason}}
{:error, reason}, {:ok, _acc} -> {:halt, {:error, reason}}
end)
end
defp fetch_transaction_receipts(_state, []), do: {:ok, %{logs: [], receipts: []}}
defp fetch_transaction_receipts(%{} = state, hashes) do
debug(state, fn -> "fetching #{length(hashes)} transaction receipts" end)
stream_opts = [max_concurrency: state.receipts_concurrency, timeout: :infinity]
hashes
|> Enum.chunk_every(state.receipts_batch_size)
|> Task.async_stream(&EthereumJSONRPC.fetch_transaction_receipts(&1), stream_opts)
|> Enum.reduce_while({:ok, %{logs: [], receipts: []}}, fn
{:ok, {:ok, %{logs: logs, receipts: receipts}}}, {:ok, %{logs: acc_logs, receipts: acc_receipts}} ->
{:cont, {:ok, %{logs: acc_logs ++ logs, receipts: acc_receipts ++ receipts}}}
{:ok, {:error, reason}}, {:ok, _acc} ->
{:halt, {:error, reason}}
{:error, reason}, {:ok, _acc} ->
{:halt, {:error, reason}}
end)
end
defp genesis_task(%{} = state) do
{count, missing_ranges} = missing_block_numbers(state)
current_block = Indexer.next_block_number()
debug(state, fn -> "#{count} missed block ranges between genesis and #{current_block}" end)
{:ok, seq} = Sequence.start_link(missing_ranges, current_block, state.blocks_batch_size)
stream_import(state, seq, max_concurrency: state.blocks_concurrency)
end
defp insert(%{} = state, seq, range, params) do
with {:ok, %{addresses: address_hashes}} = ok <- Chain.import_blocks(params) do
:ok = AddressFetcher.async_fetch_balances(address_hashes)
ok
else
{:error, step, reason} = error ->
debug(state, fn ->
"failed to insert blocks during #{step} #{inspect(range)}: #{inspect(reason)}. Retrying"
end)
:ok = Sequence.inject_range(seq, range)
error
end
end
defp missing_block_numbers(%{blocks_batch_size: blocks_batch_size}) do
{count, missing_ranges} = Chain.missing_block_numbers()
chunked_ranges =
Enum.flat_map(missing_ranges, fn
{start, ending} when ending - start <= blocks_batch_size ->
[{start, ending}]
{start, ending} ->
start
|> Stream.iterate(&(&1 + blocks_batch_size))
|> Enum.reduce_while([], fn
chunk_start, acc when chunk_start + blocks_batch_size >= ending ->
{:halt, [{chunk_start, ending} | acc]}
chunk_start, acc ->
{:cont, [{chunk_start, chunk_start + blocks_batch_size - 1} | acc]}
end)
|> Enum.reverse()
end)
{count, chunked_ranges}
end
defp realtime_task(%{} = state) do
{:ok, seq} = Sequence.start_link([], Indexer.next_block_number(), 2)
stream_import(state, seq, max_concurrency: 1)
end
defp stream_import(state, seq, task_opts) do
seq
|> Sequence.build_stream()
|> Task.async_stream(&import_range(&1, state, seq), Keyword.merge(task_opts, timeout: :infinity))
|> Stream.run()
end
# Run at state.blocks_concurrency max_concurrency when called by `stream_import/3`
# Only public for testing
@doc false
def import_range({block_start, block_end} = range, %{} = state, seq) do
with {:blocks, {:ok, next, result}} <- {:blocks, EthereumJSONRPC.fetch_blocks_by_range(block_start, block_end)},
%{blocks: blocks, transactions: transactions} = result,
cap_seq(seq, next, range, state),
transaction_hashes = Transactions.params_to_hashes(transactions),
{:receipts, {:ok, receipt_params}} <- {:receipts, fetch_transaction_receipts(state, transaction_hashes)},
%{logs: logs, receipts: receipts} = receipt_params,
{:internal_transactions, {:ok, internal_transactions}} <-
{:internal_transactions, fetch_internal_transactions(state, transaction_hashes)} do
insert(state, seq, range, %{
blocks: blocks,
internal_transactions: internal_transactions,
logs: logs,
receipts: receipts,
transactions: transactions
})
else
{step, {:error, reason}} ->
debug(state, fn ->
"failed to fetch #{step} for blocks #{block_start} - #{block_end}: #{inspect(reason)}. Retrying block range."
end)
:ok = Sequence.inject_range(seq, range)
{:error, step, reason}
end
end
defp schedule_next_catchup_index(state) do
send(self(), :catchup_index)
state
end
defp schedule_next_realtime_fetch(state) do
Process.send_after(self(), :realtime_index, state.realtime_interval)
state
end
defp monitor_task(task_func) do
{:ok, pid} = Task.Supervisor.start_child(Indexer.TaskSupervisor, task_func)
ref = Process.monitor(pid)
{:ok, pid, ref}
end
defp debug(%{debug_logs: true}, func), do: Logger.debug(func)
defp debug(%{debug_logs: false}, _func), do: :noop
end

@ -0,0 +1,80 @@
defmodule Explorer.Indexer.Sequence do
@moduledoc false
use Agent
defstruct ~w(current mode queue step)a
@type range :: {pos_integer(), pos_integer()}
@doc """
Builds an enumerable stream using a sequencer agent.
"""
@spec build_stream(pid()) :: Enumerable.t()
def build_stream(sequencer) when is_pid(sequencer) do
Stream.resource(
fn -> sequencer end,
fn seq ->
case pop(seq) do
:halt -> {:halt, seq}
range -> {[range], seq}
end
end,
fn seq -> seq end
)
end
@doc """
Changes the mode for the sequencer to signal continuous streaming mode.
"""
@spec cap(pid()) :: :ok
def cap(sequencer) when is_pid(sequencer) do
Agent.update(sequencer, fn state ->
%__MODULE__{state | mode: :finite}
end)
end
@doc """
Adds a range of block numbers to the sequence.
"""
@spec inject_range(pid(), range()) :: :ok
def inject_range(sequencer, {_first, _last} = range) when is_pid(sequencer) do
Agent.update(sequencer, fn state ->
%__MODULE__{state | queue: :queue.in(range, state.queue)}
end)
end
@doc """
Pops the next block range from the sequence.
"""
@spec pop(pid()) :: range() | :halt
def pop(sequencer) when is_pid(sequencer) do
Agent.get_and_update(sequencer, fn %__MODULE__{current: current, step: step} = state ->
case {state.mode, :queue.out(state.queue)} do
{_, {{:value, {starting, ending}}, new_queue}} ->
{{starting, ending}, %__MODULE__{state | queue: new_queue}}
{:infinite, {:empty, new_queue}} ->
{{current, current + step - 1}, %__MODULE__{state | current: current + step, queue: new_queue}}
{:finite, {:empty, new_queue}} ->
{:halt, %__MODULE__{state | queue: new_queue}}
end
end)
end
@doc """
Stars a process for managing a block sequence.
"""
@spec start_link([range()], pos_integer(), pos_integer()) :: Agent.on_start()
def start_link(initial_ranges, range_start, step) do
Agent.start_link(fn ->
%__MODULE__{
current: range_start,
step: step,
mode: :infinite,
queue: :queue.from_list(initial_ranges)
}
end)
end
end

@ -0,0 +1,24 @@
defmodule Explorer.Indexer.Supervisor do
@moduledoc """
Supervising the fetchers for the `Explorer.Indexer`
"""
use Supervisor
alias Explorer.Indexer.{AddressFetcher, BlockFetcher}
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl Supervisor
def init(_opts) do
children = [
{Task.Supervisor, name: Explorer.Indexer.TaskSupervisor},
{AddressFetcher, []},
{BlockFetcher, []}
]
Supervisor.init(children, strategy: :rest_for_one)
end
end

@ -28,8 +28,6 @@ defmodule Explorer.Market.History.Cataloger do
@typep milliseconds :: non_neg_integer() @typep milliseconds :: non_neg_integer()
## GenServer callbacks
@impl GenServer @impl GenServer
def init(:ok) do def init(:ok) do
send(self(), {:fetch_history, 365}) send(self(), {:fetch_history, 365})
@ -80,8 +78,6 @@ defmodule Explorer.Market.History.Cataloger do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end end
## Private Functions
@spec base_backoff :: milliseconds() @spec base_backoff :: milliseconds()
defp base_backoff do defp base_backoff do
config_or_default(:base_backoff, 100) config_or_default(:base_backoff, 100)

@ -12,7 +12,7 @@ defmodule Explorer.Market do
@doc """ @doc """
Get most recent exchange rate for the given symbol. Get most recent exchange rate for the given symbol.
""" """
@spec get_exchange_rate(String.t()) :: Token.t() @spec get_exchange_rate(String.t()) :: Token.t() | nil
def get_exchange_rate(symbol) do def get_exchange_rate(symbol) do
ExchangeRates.lookup(symbol) ExchangeRates.lookup(symbol)
end end

@ -13,9 +13,9 @@ defmodule Explorer.Market.MarketHistory do
@typedoc """ @typedoc """
The recorded values of the configured coin to USD for a single day. The recorded values of the configured coin to USD for a single day.
* `:closing_price` - Closing price in USD. * `:closing_price` - Closing price in USD.
* `:date` - The date in UTC. * `:date` - The date in UTC.
* `:opening_price` - Opening price in USD. * `:opening_price` - Opening price in USD.
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
closing_price: Decimal.t(), closing_price: Decimal.t(),

@ -1,7 +1,6 @@
defmodule Explorer.Repo do defmodule Explorer.Repo do
use Ecto.Repo, otp_app: :explorer use Ecto.Repo, otp_app: :explorer
use Scrivener, page_size: 10 use Scrivener, page_size: 10
@dialyzer {:nowarn_function, rollback: 1}
@doc """ @doc """
Dynamically loads the repository url from the Dynamically loads the repository url from the
@ -11,11 +10,24 @@ defmodule Explorer.Repo do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end end
defmodule NewRelic do @doc """
use NewRelixir.Plug.Repo, repo: Explorer.Repo Chunks elements into multiple `insert_all`'s to avoid DB driver param limits.
*Note:* Should always be run within a transaction as multiple inserts may occur.
"""
def safe_insert_all(kind, elements, opts) do
returning = opts[:returning]
elements
|> Enum.chunk_every(1000)
|> Enum.reduce({0, []}, fn chunk, {total_count, acc} ->
{count, inserted} = insert_all(kind, chunk, opts)
def paginate(queryable, opts \\ []) do if returning do
Explorer.Repo.paginate(queryable, opts) {count + total_count, acc ++ inserted}
end else
{count + total_count, nil}
end
end)
end end
end end

@ -1,4 +0,0 @@
defmodule Explorer.Scheduler do
@moduledoc false
use Quantum.Scheduler, otp_app: :explorer
end

@ -1,21 +0,0 @@
defmodule Explorer.SkippedBalances do
@moduledoc "Gets a list of Addresses that do not have balances."
alias Explorer.Chain.Address
alias Explorer.Repo.NewRelic, as: Repo
import Ecto.Query, only: [from: 2]
def fetch(count) do
query =
from(
address in Address,
select: address.hash,
where: is_nil(address.balance),
limit: ^count
)
query
|> Repo.all()
end
end

@ -1,32 +0,0 @@
defmodule Explorer.SkippedBlocks do
@moduledoc """
Fill in older blocks that were skipped during processing.
"""
import Ecto.Query, only: [from: 2, limit: 2]
alias Explorer.Chain.Block
alias Explorer.Repo.NewRelic, as: Repo
@missing_number_query "SELECT generate_series(?, 0, -1) AS missing_number"
def first, do: first(1)
def first(count) do
blocks =
from(
b in Block,
right_join: fragment(@missing_number_query, ^latest_block_number()),
on: b.number == fragment("missing_number"),
select: fragment("missing_number::text"),
where: is_nil(b.id),
limit: ^count
)
Repo.all(blocks)
end
def latest_block_number do
block = Repo.one(Block |> Block.latest() |> limit(1)) || Block.null()
block.number
end
end

@ -1,25 +0,0 @@
defmodule Explorer.SkippedInternalTransactions do
@moduledoc """
Find transactions that do not have internal transactions.
"""
import Ecto.Query, only: [from: 2]
alias Explorer.Chain.Transaction
alias Explorer.Repo.NewRelic, as: Repo
def first, do: first(1)
def first(count) do
transactions =
from(
transaction in Transaction,
left_join: internal_transactions in assoc(transaction, :internal_transactions),
select: fragment("hash"),
group_by: transaction.id,
having: count(internal_transactions.id) == 0,
limit: ^count
)
Repo.all(transactions)
end
end

@ -1,25 +0,0 @@
defmodule Explorer.SkippedReceipts do
@moduledoc """
Find transactions that do not have a receipt.
"""
import Ecto.Query, only: [from: 2]
alias Explorer.Chain.Transaction
alias Explorer.Repo.NewRelic, as: Repo
def first, do: first(1)
def first(count) do
transactions =
from(
transaction in Transaction,
left_join: receipt in assoc(transaction, :receipt),
select: fragment("hash"),
group_by: transaction.id,
having: count(receipt.id) == 0,
limit: ^count
)
Repo.all(transactions)
end
end

@ -1,13 +0,0 @@
defmodule Explorer.Workers.ImportBalance do
@moduledoc "A worker that imports the balance for a given address."
alias Explorer.BalanceImporter
def perform(hash) do
BalanceImporter.import(hash)
end
def perform_later(hash) do
Exq.enqueue(Exq.Enqueuer, "balances", __MODULE__, [hash])
end
end

@ -1,26 +0,0 @@
defmodule Explorer.Workers.ImportBlock do
@moduledoc "Imports blocks by web3 conventions."
import Ethereumex.HttpClient, only: [eth_block_number: 0]
alias Explorer.BlockImporter
@dialyzer {:nowarn_function, perform: 1}
def perform("latest") do
case eth_block_number() do
{:ok, number} -> perform_later(number)
_ -> nil
end
end
@dialyzer {:nowarn_function, perform: 1}
def perform(number), do: BlockImporter.import("#{number}")
def perform_later("0x" <> number) when is_binary(number) do
number |> String.to_integer(16) |> perform_later()
end
def perform_later(number) do
Exq.enqueue(Exq.Enqueuer, "blocks", __MODULE__, [number])
end
end

@ -1,12 +0,0 @@
defmodule Explorer.Workers.ImportInternalTransaction do
@moduledoc "Imports internal transactions via Parity trace endpoints."
alias Explorer.InternalTransactionImporter
@dialyzer {:nowarn_function, perform: 1}
def perform(hash), do: InternalTransactionImporter.import(hash)
def perform_later(hash) do
Exq.enqueue(Exq.Enqueuer, "internal_transactions", __MODULE__, [hash])
end
end

@ -1,12 +0,0 @@
defmodule Explorer.Workers.ImportReceipt do
@moduledoc "Imports transaction by web3 conventions."
alias Explorer.ReceiptImporter
@dialyzer {:nowarn_function, perform: 1}
def perform(hash), do: ReceiptImporter.import(hash)
def perform_later(hash) do
Exq.enqueue(Exq.Enqueuer, "receipts", __MODULE__, [hash])
end
end

@ -1,18 +0,0 @@
defmodule Explorer.Workers.ImportSkippedBlocks do
alias Explorer.SkippedBlocks
alias Explorer.Workers.ImportBlock
@moduledoc "Imports skipped blocks."
def perform, do: perform(1)
def perform(count) do
count |> SkippedBlocks.first() |> Enum.map(&ImportBlock.perform_later/1)
end
def perform_later, do: perform_later(1)
def perform_later(count) do
Exq.enqueue(Exq.Enqueuer, "default", __MODULE__, [count])
end
end

@ -1,26 +0,0 @@
defmodule Explorer.Workers.ImportTransaction do
@moduledoc """
Manages the lifecycle of importing a single Transaction from web3.
"""
alias Explorer.TransactionImporter
alias Explorer.Workers.{ImportInternalTransaction, ImportReceipt}
@dialyzer {:nowarn_function, perform: 1}
def perform(hash) when is_binary(hash) do
TransactionImporter.import(hash)
ImportInternalTransaction.perform_later(hash)
ImportReceipt.perform_later(hash)
end
@dialyzer {:nowarn_function, perform: 1}
def perform(raw_transaction) when is_map(raw_transaction) do
TransactionImporter.import(raw_transaction)
ImportInternalTransaction.perform_later(raw_transaction["hash"])
ImportReceipt.perform_later(raw_transaction["hash"])
end
def perform_later(hash) do
Exq.enqueue(Exq.Enqueuer, "transactions", __MODULE__, [hash])
end
end

@ -1,29 +0,0 @@
defmodule Explorer.Workers.RefreshBalance do
@moduledoc """
Refreshes the Credit and Debit balance views.
"""
alias Ecto.Adapters.SQL
alias Explorer.Chain.{Credit, Debit}
alias Explorer.Repo
def perform("credit"), do: unless(refreshing("credits"), do: Credit.refresh())
def perform("debit"), do: unless(refreshing("debits"), do: Debit.refresh())
def perform do
perform_later(["credit"])
perform_later(["debit"])
end
def perform_later(args \\ []) do
Exq.enqueue(Exq.Enqueuer, "default", __MODULE__, args)
end
def refreshing(table) do
query = "REFRESH MATERIALIZED VIEW CONCURRENTLY #{table}%"
result = SQL.query!(Repo, "SELECT TRUE FROM pg_stat_activity WHERE query ILIKE '$#{query}'", [])
Enum.count(result.rows) > 0
end
end

@ -1,45 +0,0 @@
defmodule GiantAddressMigrator do
@moduledoc "Migrate away from Address join tables."
require Logger
alias Explorer.Repo
def migrate do
for n <- 1..20 do
chunk_size = 500_000
lower = n * chunk_size - chunk_size
upper = n * chunk_size
Logger.info("fetching results between #{lower} and #{upper}")
work_on_transactions_between_ids(lower, upper)
end
end
def work_on_transactions_between_ids(lower, upper) do
query = """
select transactions.id, from_addresses.address_id as from_address_id, to_addresses.address_id as to_address_id
FROM transactions
inner join from_addresses on from_addresses.transaction_id = id
inner join to_addresses on to_addresses.transaction_id = id
where transactions.id >= #{lower} AND transactions.id < #{upper}
;
"""
{:ok, result} = Repo.query(query, [])
Logger.info("got em!")
result.rows
|> Enum.each(&sweet_update/1)
end
def sweet_update([transaction_id, from_address_id, to_address_id]) do
query = """
UPDATE transactions SET from_address_id = $1, to_address_id = $2 WHERE id = $3
"""
{:ok, _status} = Repo.query(query, [from_address_id, to_address_id, transaction_id])
end
def sweet_update(_), do: nil
end

@ -1,25 +0,0 @@
defmodule Mix.Tasks.Exq.Start do
@moduledoc "Starts the Exq worker"
use Mix.Task
alias Explorer.{Repo, Scheduler}
def run(["scheduler"]) do
[:postgrex, :ecto, :ethereumex, :tzdata]
|> Enum.each(&Application.ensure_all_started/1)
Repo.start_link()
Exq.start_link(mode: :enqueuer)
Scheduler.start_link()
:timer.sleep(:infinity)
end
def run(_) do
[:postgrex, :ecto, :ethereumex, :tzdata]
|> Enum.each(&Application.ensure_all_started/1)
Repo.start_link()
Exq.start_link(mode: :default)
:timer.sleep(:infinity)
end
end

@ -1,24 +0,0 @@
defmodule Mix.Tasks.Scrape.Balances do
@moduledoc "Populate Address balances."
use Mix.Task
alias Explorer.{BalanceImporter, Repo, SkippedBalances}
def run([]), do: run(1)
def run(count) do
[:postgrex, :ecto, :ethereumex, :tzdata]
|> Enum.each(&Application.ensure_all_started/1)
Repo.start_link()
Exq.start_link(mode: :enqueuer)
"#{count}"
|> String.to_integer()
|> SkippedBalances.fetch()
|> Flow.from_enumerable()
|> Flow.map(&BalanceImporter.import/1)
|> Enum.to_list()
end
end

@ -1,26 +0,0 @@
defmodule Mix.Tasks.Scrape.Blocks do
@moduledoc "Scrapes blocks from web3"
use Mix.Task
alias Explorer.{BlockImporter, Repo, SkippedBlocks}
def run([]), do: run(1)
def run(count) do
[:postgrex, :ecto, :ethereumex, :tzdata]
|> Enum.each(&Application.ensure_all_started/1)
Repo.start_link()
Exq.start_link(mode: :enqueuer)
"#{count}"
|> String.to_integer()
|> SkippedBlocks.first()
|> Enum.shuffle()
|> Flow.from_enumerable()
|> Flow.map(&BlockImporter.download_block/1)
|> Flow.map(&BlockImporter.import/1)
|> Enum.to_list()
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save