Extract Explorer.JSONRPC to its own OTP application, apps/ethereum_jsonrpc. It is not ready to be a completely separate library because it knowns the internal params format used for Explorer.Chain.pull/162/head
parent
75388c8ece
commit
11bcecad96
@ -1,18 +1,303 @@ |
|||||||
defmodule EthereumJsonrpc do |
defmodule EthereumJSONRPC do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Documentation for EthereumJsonrpc. |
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 """ |
@doc """ |
||||||
Hello world. |
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() |
||||||
|
|
||||||
## Examples |
request = %{ |
||||||
|
"id" => id, |
||||||
|
"jsonrpc" => "2.0", |
||||||
|
"method" => "eth_newBlockFilter", |
||||||
|
"params" => [] |
||||||
|
} |
||||||
|
|
||||||
iex> EthereumJsonrpc.hello |
json_rpc(request, config(:url)) |
||||||
:world |
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 hello do |
def timestamp_to_datetime(timestamp) do |
||||||
:world |
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 |
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 |
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 |
@ -1,10 +1,10 @@ |
|||||||
defmodule Explorer.JSONRPC.Logs do |
defmodule EthereumJSONRPC.Logs do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Collection of logs included in return from |
Collection of logs included in return from |
||||||
[`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt). |
[`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt). |
||||||
""" |
""" |
||||||
|
|
||||||
alias Explorer.JSONRPC.Log |
alias EthereumJSONRPC.Log |
||||||
|
|
||||||
@type elixir :: [Log.elixir()] |
@type elixir :: [Log.elixir()] |
||||||
@type t :: [Log.t()] |
@type t :: [Log.t()] |
@ -1,16 +1,16 @@ |
|||||||
defmodule Explorer.JSONRPC.Parity do |
defmodule EthereumJSONRPC.Parity do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/). |
Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/). |
||||||
""" |
""" |
||||||
|
|
||||||
import Explorer.JSONRPC, only: [config: 1, json_rpc: 2] |
import EthereumJSONRPC, only: [config: 1, json_rpc: 2] |
||||||
|
|
||||||
alias Explorer.JSONRPC.Parity.Traces |
alias EthereumJSONRPC.Parity.Traces |
||||||
|
|
||||||
@doc """ |
@doc """ |
||||||
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Parity trace URL. |
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Parity trace URL. |
||||||
|
|
||||||
iex> Explorer.JSONRPC.Parity.fetch_internal_transactions([ |
iex> EthereumJSONRPC.Parity.fetch_internal_transactions([ |
||||||
...> "0x0fa6f723216dba694337f9bb37d8870725655bdf2573526a39454685659e39b1" |
...> "0x0fa6f723216dba694337f9bb37d8870725655bdf2573526a39454685659e39b1" |
||||||
...> ]) |
...> ]) |
||||||
{:ok, |
{:ok, |
@ -1,11 +1,11 @@ |
|||||||
defmodule Explorer.JSONRPC.Parity.Traces do |
defmodule EthereumJSONRPC.Parity.Traces do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Trace returned by |
Trace returned by |
||||||
[`trace_replayTransaction`](https://wiki.parity.io/JSONRPC-trace-module.html#trace_replaytransaction), which is an |
[`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/). |
extension to the Ethereum JSONRPC standard that is only supported by [Parity](https://wiki.parity.io/). |
||||||
""" |
""" |
||||||
|
|
||||||
alias Explorer.JSONRPC.Parity.Trace |
alias EthereumJSONRPC.Parity.Trace |
||||||
|
|
||||||
def elixir_to_params(elixir) when is_list(elixir) do |
def elixir_to_params(elixir) when is_list(elixir) do |
||||||
Enum.map(elixir, &Trace.elixir_to_params/1) |
Enum.map(elixir, &Trace.elixir_to_params/1) |
@ -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 |
@ -1,13 +1,13 @@ |
|||||||
defmodule Explorer.JSONRPC.ReceiptsTest do |
defmodule EthereumJSONRPC.ReceiptsTest do |
||||||
use ExUnit.Case, async: true |
use ExUnit.Case, async: true |
||||||
|
|
||||||
alias Explorer.JSONRPC.Receipts |
alias EthereumJSONRPC.Receipts |
||||||
|
|
||||||
doctest Receipts |
doctest Receipts |
||||||
|
|
||||||
# These are integration tests that depend on the sokol chain being used. sokol can be used with the following config |
# These are integration tests that depend on the sokol chain being used. sokol can be used with the following config |
||||||
# |
# |
||||||
# config :explorer, Explorer.JSONRPC, |
# config :explorer, EthereumJSONRPC, |
||||||
# trace_url: "https://sokol-trace.poa.network", |
# trace_url: "https://sokol-trace.poa.network", |
||||||
# url: "https://sokol.poa.network" |
# url: "https://sokol.poa.network" |
||||||
# |
# |
@ -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 |
@ -1,8 +0,0 @@ |
|||||||
defmodule EthereumJsonrpcTest do |
|
||||||
use ExUnit.Case |
|
||||||
doctest EthereumJsonrpc |
|
||||||
|
|
||||||
test "greets the world" do |
|
||||||
assert EthereumJsonrpc.hello() == :world |
|
||||||
end |
|
||||||
end |
|
@ -1 +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() |
ExUnit.start() |
||||||
|
@ -1,309 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC do |
|
||||||
@moduledoc """ |
|
||||||
Ethereum JSONRPC client. |
|
||||||
|
|
||||||
## Configuration |
|
||||||
|
|
||||||
Configuration for parity URLs can be provided with the following mix config: |
|
||||||
|
|
||||||
config :explorer, Explorer.JSONRPC, |
|
||||||
url: "https://sokol.poa.network", |
|
||||||
trace_url: "https://sokol-trace.poa.network", |
|
||||||
http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :eth]] |
|
||||||
|
|
||||||
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 Explorer.JSONRPC.{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() |
|
||||||
|
|
||||||
def child_spec(_opts) do |
|
||||||
:hackney_pool.child_spec(:eth, recv_timeout: 60_000, timeout: 60_000, max_connections: 1000) |
|
||||||
end |
|
||||||
|
|
||||||
@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 :explorer, Explorer.JSONRRPC, key: value |
|
||||||
|
|
||||||
Configuration can be set a runtime using `Application.put_env/3` |
|
||||||
|
|
||||||
Application.put_env(:explorer, Explorer.JSONRPC, key: value) |
|
||||||
|
|
||||||
""" |
|
||||||
def config(key) do |
|
||||||
:explorer |
|
||||||
|> Application.fetch_env!(__MODULE__) |
|
||||||
|> Keyword.fetch!(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 |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.BlockTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Block |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.BlocksTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Blocks |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.LogTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Log |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.Parity.Trace.ActionTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Parity.Trace.Action |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.Parity.Trace.ResultTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Parity.Trace.Result |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.Parity.TraceTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Parity.Trace |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.ParityTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Parity |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.ReceiptTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Receipt |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.TransactionTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Transaction |
|
||||||
end |
|
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.JSONRPC.TransactionsTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.JSONRPC.Transactions |
|
||||||
end |
|
Loading…
Reference in new issue