Land #162: Optimized indexer
commit
d4234d3246
@ -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() |
@ -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 |
File diff suppressed because it is too large
Load Diff
@ -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 |
@ -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 |
||||
@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 """ |
||||
[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 |
||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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…
Reference in new issue