Refactor JSON RPC variants

pull/6155/head
Viktor Baranov 2 years ago
parent b5978a1f87
commit 0f4a4bd6a7
  1. 1
      CHANGELOG.md
  2. 256
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex
  3. 178
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex
  4. 266
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/erigon.ex
  5. 181
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_beneficiaries.ex
  6. 23
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex
  7. 258
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind.ex
  8. 0
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex
  9. 0
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace/action.ex
  10. 0
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace/result.ex
  11. 0
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/traces.ex
  12. 184
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex
  13. 71
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex
  14. 250
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex
  15. 2
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex
  16. 4
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/fetched_beneficiaries_test.exs
  17. 0
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace/action_test.exs
  18. 0
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace/result_test.exs
  19. 0
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace_test.exs
  20. 0
      apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex
  21. 0
      apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/mox.ex
  22. 3
      config/runtime.exs

@ -17,6 +17,7 @@
### Chore ### Chore
- [#6155](https://github.com/blockscout/blockscout/pull/6155) - Refactor Ethereum JSON RPC variants
- [#6125](https://github.com/blockscout/blockscout/pull/6125) - Rename obsolete "parity" EthereumJSONRPC.Variant to "nethermind" - [#6125](https://github.com/blockscout/blockscout/pull/6125) - Rename obsolete "parity" EthereumJSONRPC.Variant to "nethermind"
- [#6124](https://github.com/blockscout/blockscout/pull/6124) - Docker compose: add config for Erigon - [#6124](https://github.com/blockscout/blockscout/pull/6124) - Docker compose: add config for Erigon
- [#6053](https://github.com/blockscout/blockscout/pull/6053) - Bump jest-environment-jsdom from 29.0.1 to 29.0.2 in /apps/block_scout_web/assets - [#6053](https://github.com/blockscout/blockscout/pull/6053) - Bump jest-environment-jsdom from 29.0.1 to 29.0.2 in /apps/block_scout_web/assets

@ -3,11 +3,10 @@ defmodule EthereumJSONRPC.Besu do
@moduledoc """ @moduledoc """
Ethereum JSONRPC methods that are only supported by [Besu](https://besu.hyperledger.org/en/stable/Reference/API-Methods). Ethereum JSONRPC methods that are only supported by [Besu](https://besu.hyperledger.org/en/stable/Reference/API-Methods).
""" """
require Logger import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2]
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Besu.{FetchedBeneficiaries, Traces} alias EthereumJSONRPC.Besu.Traces
alias EthereumJSONRPC.{Transaction, Transactions} alias EthereumJSONRPC.{FetchedBeneficiaries, PendingTransaction, TraceReplayBlockTransactions, Transaction}
@behaviour EthereumJSONRPC.Variant @behaviour EthereumJSONRPC.Variant
@ -35,48 +34,12 @@ defmodule EthereumJSONRPC.Besu do
""" """
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do
id_to_params = id_to_params(block_numbers) TraceReplayBlockTransactions.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, Traces)
with {:ok, responses} <-
id_to_params
|> trace_replay_block_transactions_requests()
|> json_rpc(json_rpc_named_arguments) do
trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params)
end
end end
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params) TraceReplayBlockTransactions.fetch_first_trace(transactions_params, json_rpc_named_arguments, Traces)
trace_replay_transaction_response =
id_to_params
|> trace_replay_transaction_requests()
|> json_rpc(json_rpc_named_arguments)
case trace_replay_transaction_response do
{:ok, responses} ->
case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params) do
{:ok, [first_trace]} ->
%{block_hash: block_hash} =
transactions_params
|> Enum.at(0)
{:ok,
[%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]}
{:error, error} ->
Logger.error(inspect(error))
{:error, error}
end
{:error, :econnrefused} ->
{:error, :econnrefused}
{:error, [error]} ->
Logger.error(inspect(error))
{:error, error}
end
end end
@doc """ @doc """
@ -89,217 +52,10 @@ defmodule EthereumJSONRPC.Besu do
@spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) :: @spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) ::
{:ok, [Transaction.params()]} | {:error, reason :: term} {:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions(json_rpc_named_arguments) do def fetch_pending_transactions(json_rpc_named_arguments) do
with {:ok, transactions} <- PendingTransaction.fetch_pending_transactions_besu(json_rpc_named_arguments)
%{id: 1, method: "txpool_besuTransactions", params: []}
|> request()
|> json_rpc(json_rpc_named_arguments) do
transactions_params =
transactions
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
{:ok, transactions_params}
end
end end
defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do
Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)})
end end
defp trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_block_transactions_responses_to_traces(responses, id_to_params) do
params =
traces
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_block_transactions_responses_to_traces(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_block_transactions_response_to_traces(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, result: results}, id_to_params)
when is_list(results) and is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_traces =
results
|> Stream.with_index()
|> Enum.flat_map(fn {%{"trace" => traces, "transactionHash" => transaction_hash}, transaction_index} ->
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => block_number,
"transactionHash" => transaction_hash,
"transactionIndex" => transaction_index,
"index" => index
})
end)
end)
{:ok, annotated_traces}
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockNumber" => block_number
})
{:error, annotated_error}
end
defp trace_replay_block_transactions_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, block_number} ->
trace_replay_block_transactions_request(%{id: id, block_number: block_number})
end)
end
defp trace_replay_block_transactions_request(%{id: id, block_number: block_number}) do
request(%{id: id, method: "trace_replayBlockTransactions", params: [integer_to_quantity(block_number), ["trace"]]})
end
def trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_transaction_responses_to_first_trace(responses, id_to_params) do
params =
traces
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_transaction_responses_to_first_trace(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_transaction_response_to_first_trace(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, result: %{"trace" => traces}}, id_to_params)
when is_list(traces) and is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
first_trace =
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"index" => index,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
end)
|> Enum.filter(fn trace ->
Map.get(trace, "index") == 0
end)
{:ok, first_trace}
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
{:error, annotated_error}
end
defp trace_replay_transaction_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, %{hash_data: hash_data}} ->
trace_replay_transaction_request(%{id: id, hash_data: hash_data})
end)
end
defp trace_replay_transaction_request(%{id: id, hash_data: hash_data}) do
request(%{id: id, method: "trace_replayTransaction", params: [hash_data, ["trace"]]})
end
end end

@ -1,178 +0,0 @@
defmodule EthereumJSONRPC.Besu.FetchedBeneficiaries do
@moduledoc """
Beneficiaries and errors from batch requests to `trace_block`.
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
@doc """
Converts `responses` to `EthereumJSONRPC.FetchedBeneficiaries.t()`.
responses - List with trace_block responses
id_to_params - Maps request id to query params
## Examples
iex> EthereumJSONRPC.Besu.FetchedBeneficiaries.from_responses(
...> [
...> %{
...> id: 0,
...> result: [
...> %{
...> "action" => %{"author" => "0x1", "rewardType" => "external", "value" => "0x0"},
...> "blockHash" => "0xFFF",
...> "blockNumber" => 12,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => nil,
...> "transactionPosition" => nil,
...> "type" => "reward"
...> },
...> %{
...> "action" => %{"author" => "0x2", "rewardType" => "external", "value" => "0x0"},
...> "blockHash" => "0xFFF",
...> "blockNumber" => 12,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => nil,
...> "transactionPosition" => nil,
...> "type" => "reward"
...> }
...> ]
...> }
...> ],
...> %{0 => %{block_quantity: "0xC"}}
...> )
%EthereumJSONRPC.FetchedBeneficiaries{
errors: [],
params_set: #MapSet<[
%{
address_hash: "0x1",
address_type: :validator,
block_hash: "0xFFF",
block_number: 12,
reward: "0x0"
},
%{
address_hash: "0x2",
address_type: :emission_funds,
block_hash: "0xFFF",
block_number: 12,
reward: "0x0"
}
]>
}
"""
def from_responses(responses, id_to_params) when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&response_to_params_set(&1, id_to_params))
|> Enum.reduce(
%EthereumJSONRPC.FetchedBeneficiaries{},
fn
{:ok, params_set}, %EthereumJSONRPC.FetchedBeneficiaries{params_set: acc_params_set} = acc ->
%EthereumJSONRPC.FetchedBeneficiaries{acc | params_set: MapSet.union(acc_params_set, params_set)}
{:error, reason}, %EthereumJSONRPC.FetchedBeneficiaries{errors: errors} = acc ->
%EthereumJSONRPC.FetchedBeneficiaries{acc | errors: [reason | errors]}
end
)
end
@doc """
`trace_block` requests for `id_to_params`.
"""
def requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, %{block_quantity: block_quantity}} ->
request(%{id: id, block_quantity: block_quantity})
end)
end
@spec response_to_params_set(%{id: id, result: nil}, %{id => %{block_quantity: block_quantity}}) ::
{:error, %{code: 404, message: String.t(), data: %{block_quantity: block_quantity}}}
when id: non_neg_integer(), block_quantity: String.t()
defp response_to_params_set(%{id: id, result: nil}, id_to_params) when is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
{:error, %{code: 404, message: "Not Found", data: %{block_quantity: block_quantity}}}
end
@spec response_to_params_set(%{id: id, result: list(map())}, %{id => %{block_quantity: block_quantity}}) ::
{:ok, MapSet.t(EthereumJSONRPC.FetchedBeneficiary.params())}
when id: non_neg_integer(), block_quantity: String.t()
defp response_to_params_set(%{id: id, result: traces}, id_to_params) when is_list(traces) and is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
block_number = quantity_to_integer(block_quantity)
params_set = traces_to_params_set(traces, block_number)
{:ok, params_set}
end
@spec response_to_params_set(%{id: id, error: %{code: code, message: message}}, %{
id => %{block_quantity: block_quantity}
}) :: {:error, %{code: code, message: message, data: %{block_quantity: block_quantity}}}
when id: non_neg_integer(), code: integer(), message: String.t(), block_quantity: String.t()
defp response_to_params_set(%{id: id, error: error}, id_to_params) when is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
annotated_error = Map.put(error, :data, %{block_quantity: block_quantity})
{:error, annotated_error}
end
defp request(%{id: id, block_quantity: block_quantity}) when is_integer(id) and is_binary(block_quantity) do
EthereumJSONRPC.request(%{id: id, method: "trace_block", params: [block_quantity]})
end
defp traces_to_params_set(traces, block_number) when is_list(traces) and is_integer(block_number) do
traces
|> Stream.filter(&(&1["type"] == "reward"))
|> Stream.with_index()
|> Enum.reduce(MapSet.new(), fn {trace, index}, acc ->
MapSet.union(acc, trace_to_params_set(trace, block_number, index))
end)
end
defp trace_to_params_set(
%{
"action" => %{
"rewardType" => reward_type,
"author" => address_hash_data,
"value" => reward_value
},
"blockHash" => block_hash,
"blockNumber" => block_number
},
block_number,
index
)
when is_integer(block_number) and reward_type in ~w(block external uncle) do
MapSet.new([
%{
address_hash: address_hash_data,
block_hash: block_hash,
block_number: block_number,
reward: reward_value,
address_type: get_address_type(reward_type, index)
}
])
end
# Beneficiary's address type will depend on the responses' action.rewardType,
# which will vary depending on which network is being indexed
#
# On POA networks, rewardType will always be external and the type of the address being
# rewarded will depend on its position.
# First address will always be the validator's while the second will be the EmissionsFunds address
#
# On PoW networks, like Ethereum, the reward type will already specify the type for the
# address being rewarded
# The rewardType "block" will show the reward for the consensus block validator
# The rewardType "uncle" will show reward for validating an uncle block
defp get_address_type(reward_type, index) when reward_type == "external" and index == 0, do: :validator
defp get_address_type(reward_type, index) when reward_type == "external" and index == 1, do: :emission_funds
defp get_address_type(reward_type, index) when reward_type == "external" and index >= 2, do: :validator
defp get_address_type(reward_type, _index) when reward_type == "block", do: :validator
defp get_address_type(reward_type, _index) when reward_type == "uncle", do: :uncle
defp get_address_type(reward_type, _index) when reward_type == "emptyStep", do: :validator
end

@ -3,11 +3,10 @@ defmodule EthereumJSONRPC.Erigon do
@moduledoc """ @moduledoc """
Ethereum JSONRPC methods that are only supported by Erigon. Ethereum JSONRPC methods that are only supported by Erigon.
""" """
require Logger import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2]
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Nethermind.{FetchedBeneficiaries, Traces} alias EthereumJSONRPC.Nethermind.Traces
alias EthereumJSONRPC.Transactions alias EthereumJSONRPC.{FetchedBeneficiaries, PendingTransaction, TraceReplayBlockTransactions}
@behaviour EthereumJSONRPC.Variant @behaviour EthereumJSONRPC.Variant
@ -35,48 +34,12 @@ defmodule EthereumJSONRPC.Erigon do
""" """
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do
id_to_params = id_to_params(block_numbers) TraceReplayBlockTransactions.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, Traces)
with {:ok, responses} <-
id_to_params
|> trace_replay_block_transactions_requests()
|> json_rpc(json_rpc_named_arguments) do
trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params)
end
end end
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params) TraceReplayBlockTransactions.fetch_first_trace(transactions_params, json_rpc_named_arguments, Traces)
trace_replay_transaction_response =
id_to_params
|> trace_replay_transaction_requests()
|> json_rpc(json_rpc_named_arguments)
case trace_replay_transaction_response do
{:ok, responses} ->
case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params) do
{:ok, [first_trace]} ->
%{block_hash: block_hash} =
transactions_params
|> Enum.at(0)
{:ok,
[%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]}
{:error, error} ->
Logger.error(inspect(error))
{:error, error}
end
{:error, :econnrefused} ->
{:error, :econnrefused}
{:error, [error]} ->
Logger.error(inspect(error))
{:error, error}
end
end end
@doc """ @doc """
@ -84,227 +47,10 @@ defmodule EthereumJSONRPC.Erigon do
""" """
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_pending_transactions(json_rpc_named_arguments) do def fetch_pending_transactions(json_rpc_named_arguments) do
with {:ok, transaction_data} <- PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments)
%{id: 1, method: "txpool_content", params: []} |> request() |> json_rpc(json_rpc_named_arguments) do
transactions_params =
transaction_data["pending"]
|> Enum.flat_map(fn {_address, nonce_transactions_map} ->
nonce_transactions_map
|> Enum.map(fn {_nonce, transaction} ->
transaction
end)
end)
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
|> Enum.map(fn params ->
# txpool_content always returns transaction with 0x0000000000000000000000000000000000000000000000000000000000000000 value in block hash and index is null.
# https://github.com/ethereum/go-ethereum/issues/19897
params
|> Map.merge(%{:block_hash => nil, :index => nil})
end)
{:ok, transactions_params}
end
end end
defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do
Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)})
end end
defp trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_block_transactions_responses_to_traces(responses, id_to_params) do
params =
traces
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_block_transactions_responses_to_traces(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_block_transactions_response_to_traces(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, result: results}, id_to_params)
when is_list(results) and is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_traces =
results
|> Stream.with_index()
|> Enum.flat_map(fn {%{"trace" => traces, "transactionHash" => transaction_hash}, transaction_index} ->
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => block_number,
"transactionHash" => transaction_hash,
"transactionIndex" => transaction_index,
"index" => index
})
end)
end)
{:ok, annotated_traces}
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockNumber" => block_number
})
{:error, annotated_error}
end
defp trace_replay_block_transactions_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, block_number} ->
trace_replay_block_transactions_request(%{id: id, block_number: block_number})
end)
end
defp trace_replay_block_transactions_request(%{id: id, block_number: block_number}) do
request(%{id: id, method: "trace_replayBlockTransactions", params: [integer_to_quantity(block_number), ["trace"]]})
end
def trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_transaction_responses_to_first_trace(responses, id_to_params) do
params =
traces
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_transaction_responses_to_first_trace(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_transaction_response_to_first_trace(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, result: %{"trace" => traces}}, id_to_params)
when is_list(traces) and is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
first_trace =
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"index" => index,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
end)
|> Enum.filter(fn trace ->
Map.get(trace, "index") == 0
end)
{:ok, first_trace}
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
{:error, annotated_error}
end
defp trace_replay_transaction_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, %{hash_data: hash_data}} ->
trace_replay_transaction_request(%{id: id, hash_data: hash_data})
end)
end
defp trace_replay_transaction_request(%{id: id, hash_data: hash_data}) do
request(%{id: id, method: "trace_replayTransaction", params: [hash_data, ["trace"]]})
end
end end

@ -1,8 +1,10 @@
defmodule EthereumJSONRPC.FetchedBeneficiaries do defmodule EthereumJSONRPC.FetchedBeneficiaries do
@moduledoc """ @moduledoc """
Balance params and errors from a batch request to fetch beneficiaries. Beneficiaries and errors from batch requests to `trace_block`.
""" """
import EthereumJSONRPC, only: [quantity_to_integer: 1]
alias EthereumJSONRPC.FetchedBeneficiary alias EthereumJSONRPC.FetchedBeneficiary
defstruct params_set: MapSet.new(), defstruct params_set: MapSet.new(),
@ -13,4 +15,181 @@ defmodule EthereumJSONRPC.FetchedBeneficiaries do
* `errors` - all the errors from requests that failed in the batch. * `errors` - all the errors from requests that failed in the batch.
""" """
@type t :: %__MODULE__{params_set: MapSet.t(FetchedBeneficiary.params()), errors: [FetchedBeneficiary.error()]} @type t :: %__MODULE__{params_set: MapSet.t(FetchedBeneficiary.params()), errors: [FetchedBeneficiary.error()]}
@doc """
Converts `responses` to `EthereumJSONRPC.FetchedBeneficiaries.t()`.
responses - List with trace_block responses
id_to_params - Maps request id to query params
## Examples
iex> EthereumJSONRPC.FetchedBeneficiaries.from_responses(
...> [
...> %{
...> id: 0,
...> result: [
...> %{
...> "action" => %{"author" => "0x1", "rewardType" => "external", "value" => "0x0"},
...> "blockHash" => "0xFFF",
...> "blockNumber" => 12,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => nil,
...> "transactionPosition" => nil,
...> "type" => "reward"
...> },
...> %{
...> "action" => %{"author" => "0x2", "rewardType" => "external", "value" => "0x0"},
...> "blockHash" => "0xFFF",
...> "blockNumber" => 12,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => nil,
...> "transactionPosition" => nil,
...> "type" => "reward"
...> }
...> ]
...> }
...> ],
...> %{0 => %{block_quantity: "0xC"}}
...> )
%EthereumJSONRPC.FetchedBeneficiaries{
errors: [],
params_set: #MapSet<[
%{
address_hash: "0x1",
address_type: :validator,
block_hash: "0xFFF",
block_number: 12,
reward: "0x0"
},
%{
address_hash: "0x2",
address_type: :emission_funds,
block_hash: "0xFFF",
block_number: 12,
reward: "0x0"
}
]>
}
"""
def from_responses(responses, id_to_params) when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&response_to_params_set(&1, id_to_params))
|> Enum.reduce(
%EthereumJSONRPC.FetchedBeneficiaries{},
fn
{:ok, params_set}, %EthereumJSONRPC.FetchedBeneficiaries{params_set: acc_params_set} = acc ->
%EthereumJSONRPC.FetchedBeneficiaries{acc | params_set: MapSet.union(acc_params_set, params_set)}
{:error, reason}, %EthereumJSONRPC.FetchedBeneficiaries{errors: errors} = acc ->
%EthereumJSONRPC.FetchedBeneficiaries{acc | errors: [reason | errors]}
end
)
end
@doc """
`trace_block` requests for `id_to_params`.
"""
def requests(id_to_params) when is_map(id_to_params) do
id_to_params
|> Enum.map(fn {id, %{block_quantity: block_quantity}} ->
request(%{id: id, block_quantity: block_quantity})
end)
|> Enum.filter(&(!is_nil(&1)))
end
@spec response_to_params_set(%{id: id, result: nil}, %{id => %{block_quantity: block_quantity}}) ::
{:error, %{code: 404, message: String.t(), data: %{block_quantity: block_quantity}}}
when id: non_neg_integer(), block_quantity: String.t()
defp response_to_params_set(%{id: id, result: nil}, id_to_params) when is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
{:error, %{code: 404, message: "Not Found", data: %{block_quantity: block_quantity}}}
end
@spec response_to_params_set(%{id: id, result: list(map())}, %{id => %{block_quantity: block_quantity}}) ::
{:ok, MapSet.t(EthereumJSONRPC.FetchedBeneficiary.params())}
when id: non_neg_integer(), block_quantity: String.t()
defp response_to_params_set(%{id: id, result: traces}, id_to_params) when is_list(traces) and is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
block_number = quantity_to_integer(block_quantity)
params_set = traces_to_params_set(traces, block_number)
{:ok, params_set}
end
@spec response_to_params_set(%{id: id, error: %{code: code, message: message}}, %{
id => %{block_quantity: block_quantity}
}) :: {:error, %{code: code, message: message, data: %{block_quantity: block_quantity}}}
when id: non_neg_integer(), code: integer(), message: String.t(), block_quantity: String.t()
defp response_to_params_set(%{id: id, error: error}, id_to_params) when is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
annotated_error = Map.put(error, :data, %{block_quantity: block_quantity})
{:error, annotated_error}
end
defp request(%{id: id, block_quantity: block_quantity}) when is_integer(id) and is_binary(block_quantity) do
if block_quantity == "0x0" do
nil
else
EthereumJSONRPC.request(%{id: id, method: "trace_block", params: [block_quantity]})
end
end
defp traces_to_params_set(traces, block_number) when is_list(traces) and is_integer(block_number) do
traces
|> Stream.filter(&(&1["type"] == "reward"))
|> Stream.with_index()
|> Enum.reduce(MapSet.new(), fn {trace, index}, acc ->
MapSet.union(acc, trace_to_params_set(trace, block_number, index))
end)
end
defp trace_to_params_set(
%{
"action" => %{
"rewardType" => reward_type,
"author" => address_hash_data,
"value" => reward_value
},
"blockHash" => block_hash,
"blockNumber" => block_number
},
block_number,
index
)
when is_integer(block_number) and reward_type in ~w(block external uncle) do
MapSet.new([
%{
address_hash: address_hash_data,
block_hash: block_hash,
block_number: block_number,
reward: reward_value,
address_type: get_address_type(reward_type, index)
}
])
end
# Beneficiary's address type will depend on the responses' action.rewardType,
# which will vary depending on which network is being indexed
#
# On POA networks, rewardType will always be external and the type of the address being
# rewarded will depend on its position.
# First address will always be the validator's while the second will be the EmissionsFunds address
#
# On PoW networks, like Ethereum, the reward type will already specify the type for the
# address being rewarded
# The rewardType "block" will show the reward for the consensus block validator
# The rewardType "uncle" will show reward for validating an uncle block
defp get_address_type(reward_type, index) when reward_type == "external" and index == 0, do: :validator
defp get_address_type(reward_type, index) when reward_type == "external" and index == 1, do: :emission_funds
defp get_address_type(reward_type, index) when reward_type == "external" and index >= 2, do: :validator
defp get_address_type(reward_type, _index) when reward_type == "block", do: :validator
defp get_address_type(reward_type, _index) when reward_type == "uncle", do: :uncle
defp get_address_type(reward_type, _index) when reward_type == "emptyStep", do: :validator
end end

@ -5,7 +5,7 @@ defmodule EthereumJSONRPC.Geth do
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.{FetchedBalance, FetchedCode, Transactions} alias EthereumJSONRPC.{FetchedBalance, FetchedCode, PendingTransaction}
alias EthereumJSONRPC.Geth.{Calls, Tracer} alias EthereumJSONRPC.Geth.{Calls, Tracer}
@behaviour EthereumJSONRPC.Variant @behaviour EthereumJSONRPC.Variant
@ -56,26 +56,7 @@ defmodule EthereumJSONRPC.Geth do
""" """
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_pending_transactions(json_rpc_named_arguments) do def fetch_pending_transactions(json_rpc_named_arguments) do
with {:ok, transaction_data} <- PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments)
%{id: 1, method: "txpool_content", params: []} |> request() |> json_rpc(json_rpc_named_arguments) do
transactions_params =
transaction_data["pending"]
|> Enum.flat_map(fn {_address, nonce_transactions_map} ->
nonce_transactions_map
|> Enum.map(fn {_nonce, transaction} ->
transaction
end)
end)
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
|> Enum.map(fn params ->
# txpool_content always returns transaction with 0x0000000000000000000000000000000000000000000000000000000000000000 value in block hash and index is null.
# https://github.com/ethereum/go-ethereum/issues/19897
%{params | block_hash: nil, index: nil}
end)
{:ok, transactions_params}
end
end end
defp debug_trace_transaction_requests(id_to_params) when is_map(id_to_params) do defp debug_trace_transaction_requests(id_to_params) when is_map(id_to_params) do

@ -3,11 +3,10 @@ defmodule EthereumJSONRPC.Nethermind do
@moduledoc """ @moduledoc """
Ethereum JSONRPC methods that are only supported by Nethermind. Ethereum JSONRPC methods that are only supported by Nethermind.
""" """
require Logger import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2]
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Nethermind.{FetchedBeneficiaries, Traces} alias EthereumJSONRPC.Nethermind.Traces
alias EthereumJSONRPC.{Transaction, Transactions} alias EthereumJSONRPC.{FetchedBeneficiaries, PendingTransaction, TraceReplayBlockTransactions, Transaction}
@behaviour EthereumJSONRPC.Variant @behaviour EthereumJSONRPC.Variant
@ -35,48 +34,12 @@ defmodule EthereumJSONRPC.Nethermind do
""" """
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do
id_to_params = id_to_params(block_numbers) TraceReplayBlockTransactions.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, Traces)
with {:ok, responses} <-
id_to_params
|> trace_replay_block_transactions_requests()
|> json_rpc(json_rpc_named_arguments) do
trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params)
end
end end
@impl EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant
def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params) TraceReplayBlockTransactions.fetch_first_trace(transactions_params, json_rpc_named_arguments, Traces)
trace_replay_transaction_response =
id_to_params
|> trace_replay_transaction_requests()
|> json_rpc(json_rpc_named_arguments)
case trace_replay_transaction_response do
{:ok, responses} ->
case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params) do
{:ok, [first_trace]} ->
%{block_hash: block_hash} =
transactions_params
|> Enum.at(0)
{:ok,
[%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]}
{:error, error} ->
Logger.error(inspect(error))
{:error, error}
end
{:error, :econnrefused} ->
{:error, :econnrefused}
{:error, [error]} ->
Logger.error(inspect(error))
{:error, error}
end
end end
@doc """ @doc """
@ -89,217 +52,14 @@ defmodule EthereumJSONRPC.Nethermind do
@spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) :: @spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) ::
{:ok, [Transaction.params()]} | {:error, reason :: term} {:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions(json_rpc_named_arguments) do def fetch_pending_transactions(json_rpc_named_arguments) do
with {:ok, transactions} <- if Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction)[:type] == "geth" do
%{id: 1, method: "parity_pendingTransactions", params: []} PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments)
|> request() else
|> json_rpc(json_rpc_named_arguments) do PendingTransaction.fetch_pending_transactions_parity(json_rpc_named_arguments)
transactions_params =
transactions
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
{:ok, transactions_params}
end end
end end
defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do
Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)})
end end
defp trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_block_transactions_responses_to_traces(responses, id_to_params) do
params =
traces
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_block_transactions_responses_to_traces(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_block_transactions_response_to_traces(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, result: results}, id_to_params)
when is_list(results) and is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_traces =
results
|> Stream.with_index()
|> Enum.flat_map(fn {%{"trace" => traces, "transactionHash" => transaction_hash}, transaction_index} ->
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => block_number,
"transactionHash" => transaction_hash,
"transactionIndex" => transaction_index,
"index" => index
})
end)
end)
{:ok, annotated_traces}
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockNumber" => block_number
})
{:error, annotated_error}
end
defp trace_replay_block_transactions_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, block_number} ->
trace_replay_block_transactions_request(%{id: id, block_number: block_number})
end)
end
defp trace_replay_block_transactions_request(%{id: id, block_number: block_number}) do
request(%{id: id, method: "trace_replayBlockTransactions", params: [integer_to_quantity(block_number), ["trace"]]})
end
def trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_transaction_responses_to_first_trace(responses, id_to_params) do
params =
traces
|> Traces.to_elixir()
|> Traces.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_transaction_responses_to_first_trace(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_transaction_response_to_first_trace(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, result: %{"trace" => traces}}, id_to_params)
when is_list(traces) and is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
first_trace =
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"index" => index,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
end)
|> Enum.filter(fn trace ->
Map.get(trace, "index") == 0
end)
{:ok, first_trace}
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
{:error, annotated_error}
end
defp trace_replay_transaction_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, %{hash_data: hash_data}} ->
trace_replay_transaction_request(%{id: id, hash_data: hash_data})
end)
end
defp trace_replay_transaction_request(%{id: id, hash_data: hash_data}) do
request(%{id: id, method: "trace_replayTransaction", params: [hash_data, ["trace"]]})
end
end end

@ -1,184 +0,0 @@
defmodule EthereumJSONRPC.Nethermind.FetchedBeneficiaries do
@moduledoc """
Beneficiaries and errors from batch requests to `trace_block`.
"""
import EthereumJSONRPC, only: [quantity_to_integer: 1]
@doc """
Converts `responses` to `EthereumJSONRPC.FetchedBeneficiaries.t()`.
responses - List with trace_block responses
id_to_params - Maps request id to query params
## Examples
iex> EthereumJSONRPC.Nethermind.FetchedBeneficiaries.from_responses(
...> [
...> %{
...> id: 0,
...> result: [
...> %{
...> "action" => %{"author" => "0x1", "rewardType" => "external", "value" => "0x0"},
...> "blockHash" => "0xFFF",
...> "blockNumber" => 12,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => nil,
...> "transactionPosition" => nil,
...> "type" => "reward"
...> },
...> %{
...> "action" => %{"author" => "0x2", "rewardType" => "external", "value" => "0x0"},
...> "blockHash" => "0xFFF",
...> "blockNumber" => 12,
...> "result" => nil,
...> "subtraces" => 0,
...> "traceAddress" => [],
...> "transactionHash" => nil,
...> "transactionPosition" => nil,
...> "type" => "reward"
...> }
...> ]
...> }
...> ],
...> %{0 => %{block_quantity: "0xC"}}
...> )
%EthereumJSONRPC.FetchedBeneficiaries{
errors: [],
params_set: #MapSet<[
%{
address_hash: "0x1",
address_type: :validator,
block_hash: "0xFFF",
block_number: 12,
reward: "0x0"
},
%{
address_hash: "0x2",
address_type: :emission_funds,
block_hash: "0xFFF",
block_number: 12,
reward: "0x0"
}
]>
}
"""
def from_responses(responses, id_to_params) when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&response_to_params_set(&1, id_to_params))
|> Enum.reduce(
%EthereumJSONRPC.FetchedBeneficiaries{},
fn
{:ok, params_set}, %EthereumJSONRPC.FetchedBeneficiaries{params_set: acc_params_set} = acc ->
%EthereumJSONRPC.FetchedBeneficiaries{acc | params_set: MapSet.union(acc_params_set, params_set)}
{:error, reason}, %EthereumJSONRPC.FetchedBeneficiaries{errors: errors} = acc ->
%EthereumJSONRPC.FetchedBeneficiaries{acc | errors: [reason | errors]}
end
)
end
@doc """
`trace_block` requests for `id_to_params`.
"""
def requests(id_to_params) when is_map(id_to_params) do
id_to_params
|> Enum.map(fn {id, %{block_quantity: block_quantity}} ->
request(%{id: id, block_quantity: block_quantity})
end)
|> Enum.filter(&(!is_nil(&1)))
end
@spec response_to_params_set(%{id: id, result: nil}, %{id => %{block_quantity: block_quantity}}) ::
{:error, %{code: 404, message: String.t(), data: %{block_quantity: block_quantity}}}
when id: non_neg_integer(), block_quantity: String.t()
defp response_to_params_set(%{id: id, result: nil}, id_to_params) when is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
{:error, %{code: 404, message: "Not Found", data: %{block_quantity: block_quantity}}}
end
@spec response_to_params_set(%{id: id, result: list(map())}, %{id => %{block_quantity: block_quantity}}) ::
{:ok, MapSet.t(EthereumJSONRPC.FetchedBeneficiary.params())}
when id: non_neg_integer(), block_quantity: String.t()
defp response_to_params_set(%{id: id, result: traces}, id_to_params) when is_list(traces) and is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
block_number = quantity_to_integer(block_quantity)
params_set = traces_to_params_set(traces, block_number)
{:ok, params_set}
end
@spec response_to_params_set(%{id: id, error: %{code: code, message: message}}, %{
id => %{block_quantity: block_quantity}
}) :: {:error, %{code: code, message: message, data: %{block_quantity: block_quantity}}}
when id: non_neg_integer(), code: integer(), message: String.t(), block_quantity: String.t()
defp response_to_params_set(%{id: id, error: error}, id_to_params) when is_map(id_to_params) do
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id)
annotated_error = Map.put(error, :data, %{block_quantity: block_quantity})
{:error, annotated_error}
end
defp request(%{id: id, block_quantity: block_quantity}) when is_integer(id) and is_binary(block_quantity) do
if block_quantity == "0x0" do
nil
else
EthereumJSONRPC.request(%{id: id, method: "trace_block", params: [block_quantity]})
end
end
defp traces_to_params_set(traces, block_number) when is_list(traces) and is_integer(block_number) do
traces
|> Stream.filter(&(&1["type"] == "reward"))
|> Stream.with_index()
|> Enum.reduce(MapSet.new(), fn {trace, index}, acc ->
MapSet.union(acc, trace_to_params_set(trace, block_number, index))
end)
end
defp trace_to_params_set(
%{
"action" => %{
"rewardType" => reward_type,
"author" => address_hash_data,
"value" => reward_value
},
"blockHash" => block_hash,
"blockNumber" => block_number
},
block_number,
index
)
when is_integer(block_number) and reward_type in ~w(block external uncle) do
MapSet.new([
%{
address_hash: address_hash_data,
block_hash: block_hash,
block_number: block_number,
reward: reward_value,
address_type: get_address_type(reward_type, index)
}
])
end
# Beneficiary's address type will depend on the responses' action.rewardType,
# which will vary depending on which network is being indexed
#
# On POA networks, rewardType will always be external and the type of the address being
# rewarded will depend on its position.
# First address will always be the validator's while the second will be the EmissionsFunds address
#
# On PoW networks, like Ethereum, the reward type will already specify the type for the
# address being rewarded
# The rewardType "block" will show the reward for the consensus block validator
# The rewardType "uncle" will show reward for validating an uncle block
defp get_address_type(reward_type, index) when reward_type == "external" and index == 0, do: :validator
defp get_address_type(reward_type, index) when reward_type == "external" and index == 1, do: :emission_funds
defp get_address_type(reward_type, index) when reward_type == "external" and index >= 2, do: :validator
defp get_address_type(reward_type, _index) when reward_type == "block", do: :validator
defp get_address_type(reward_type, _index) when reward_type == "uncle", do: :uncle
defp get_address_type(reward_type, _index) when reward_type == "emptyStep", do: :validator
end

@ -0,0 +1,71 @@
defmodule EthereumJSONRPC.PendingTransaction do
@moduledoc """
Defines pending transactions fetching functions
"""
import EthereumJSONRPC, only: [json_rpc: 2, request: 1]
alias EthereumJSONRPC.{Transaction, Transactions}
@doc """
Geth-style fetching of pending transactions (from `txpool_content`)
"""
@spec fetch_pending_transactions_geth(EthereumJSONRPC.json_rpc_named_arguments()) ::
{:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions_geth(json_rpc_named_arguments) do
with {:ok, transaction_data} <-
%{id: 1, method: "txpool_content", params: []} |> request() |> json_rpc(json_rpc_named_arguments) do
transactions_params =
transaction_data["pending"]
|> Enum.flat_map(fn {_address, nonce_transactions_map} ->
nonce_transactions_map
|> Enum.map(fn {_nonce, transaction} ->
transaction
end)
end)
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
|> Enum.map(fn params ->
# txpool_content always returns transaction with 0x0000000000000000000000000000000000000000000000000000000000000000 value in block hash and index is null.
# https://github.com/ethereum/go-ethereum/issues/19897
%{params | block_hash: nil, index: nil}
end)
{:ok, transactions_params}
end
end
@doc """
Зфкшен-style fetching of pending transactions (from `parity_pendingTransactions`)
"""
@spec fetch_pending_transactions_parity(EthereumJSONRPC.json_rpc_named_arguments()) ::
{:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions_parity(json_rpc_named_arguments) do
with {:ok, transactions} <-
%{id: 1, method: "parity_pendingTransactions", params: []}
|> request()
|> json_rpc(json_rpc_named_arguments) do
transactions_params =
transactions
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
{:ok, transactions_params}
end
end
@spec fetch_pending_transactions_besu(EthereumJSONRPC.json_rpc_named_arguments()) ::
{:ok, [Transaction.params()]} | {:error, reason :: term}
def fetch_pending_transactions_besu(json_rpc_named_arguments) do
with {:ok, transactions} <-
%{id: 1, method: "txpool_besuTransactions", params: []}
|> request()
|> json_rpc(json_rpc_named_arguments) do
transactions_params =
transactions
|> Transactions.to_elixir()
|> Transactions.elixir_to_params()
{:ok, transactions_params}
end
end
end

@ -0,0 +1,250 @@
defmodule EthereumJSONRPC.TraceReplayBlockTransactions do
@moduledoc """
Methods for processing the data from `trace_replayBlockTransactions` JSON RPC method
"""
require Logger
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
def fetch_first_trace(transactions_params, json_rpc_named_arguments, traces_module)
when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params)
trace_replay_transaction_response =
id_to_params
|> trace_replay_transaction_requests()
|> json_rpc(json_rpc_named_arguments)
case trace_replay_transaction_response do
{:ok, responses} ->
case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params, traces_module) do
{:ok, [first_trace]} ->
%{block_hash: block_hash} =
transactions_params
|> Enum.at(0)
{:ok,
[%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]}
{:error, error} ->
Logger.error(inspect(error))
{:error, error}
end
{:error, :econnrefused} ->
{:error, :econnrefused}
{:error, [error]} ->
Logger.error(inspect(error))
{:error, error}
end
end
def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, traces_module)
when is_list(block_numbers) do
id_to_params = id_to_params(block_numbers)
with {:ok, responses} <-
id_to_params
|> trace_replay_block_transactions_requests()
|> json_rpc(json_rpc_named_arguments) do
trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params, traces_module)
end
end
defp trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params, traces_module)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_block_transactions_responses_to_traces(responses, id_to_params) do
params =
traces
|> traces_module.to_elixir()
|> traces_module.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_block_transactions_responses_to_traces(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_block_transactions_response_to_traces(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, result: results}, id_to_params)
when is_list(results) and is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_traces =
results
|> Stream.with_index()
|> Enum.flat_map(fn {%{"trace" => traces, "transactionHash" => transaction_hash}, transaction_index} ->
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockNumber" => block_number,
"transactionHash" => transaction_hash,
"transactionIndex" => transaction_index,
"index" => index
})
end)
end)
{:ok, annotated_traces}
end
defp trace_replay_block_transactions_response_to_traces(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
block_number = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockNumber" => block_number
})
{:error, annotated_error}
end
defp trace_replay_block_transactions_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, block_number} ->
trace_replay_block_transactions_request(%{id: id, block_number: block_number})
end)
end
defp trace_replay_block_transactions_request(%{id: id, block_number: block_number}) do
request(%{id: id, method: "trace_replayBlockTransactions", params: [integer_to_quantity(block_number), ["trace"]]})
end
def trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params, traces_module)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_transaction_responses_to_first_trace(responses, id_to_params) do
params =
traces
|> traces_module.to_elixir()
|> traces_module.elixir_to_params()
{:ok, params}
end
end
defp trace_replay_transaction_responses_to_first_trace(responses, id_to_params)
when is_list(responses) and is_map(id_to_params) do
responses
|> Enum.map(&trace_replay_transaction_response_to_first_trace(&1, id_to_params))
|> Enum.reduce(
{:ok, []},
fn
{:ok, traces}, {:ok, acc_traces_list} ->
{:ok, [traces | acc_traces_list]}
{:ok, _}, {:error, _} = acc_error ->
acc_error
{:error, reason}, {:ok, _} ->
{:error, [reason]}
{:error, reason}, {:error, acc_reason} ->
{:error, [reason | acc_reason]}
end
)
|> case do
{:ok, traces_list} ->
traces =
traces_list
|> Enum.reverse()
|> List.flatten()
{:ok, traces}
{:error, reverse_reasons} ->
reasons = Enum.reverse(reverse_reasons)
{:error, reasons}
end
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, result: %{"trace" => traces}}, id_to_params)
when is_list(traces) and is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
first_trace =
traces
|> Stream.with_index()
|> Enum.map(fn {trace, index} ->
Map.merge(trace, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"index" => index,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
end)
|> Enum.filter(fn trace ->
Map.get(trace, "index") == 0
end)
{:ok, first_trace}
end
defp trace_replay_transaction_response_to_first_trace(%{id: id, error: error}, id_to_params)
when is_map(id_to_params) do
%{
block_hash: block_hash,
block_number: block_number,
hash_data: transaction_hash,
transaction_index: transaction_index
} = Map.fetch!(id_to_params, id)
annotated_error =
Map.put(error, :data, %{
"blockHash" => block_hash,
"blockNumber" => block_number,
"transactionIndex" => transaction_index,
"transactionHash" => transaction_hash
})
{:error, annotated_error}
end
defp trace_replay_transaction_requests(id_to_params) when is_map(id_to_params) do
Enum.map(id_to_params, fn {id, %{hash_data: hash_data}} ->
trace_replay_transaction_request(%{id: id, hash_data: hash_data})
end)
end
defp trace_replay_transaction_request(%{id: id, hash_data: hash_data}) do
request(%{id: id, method: "trace_replayTransaction", params: [hash_data, ["trace"]]})
end
end

@ -22,7 +22,7 @@ defmodule EthereumJSONRPC.Variant do
## Returns ## Returns
* `{:ok, %EthereumJSONRPC.FetchedBeneficiaries{params_list: [%{address_hash: address_hash, block_number: block_number}], errors: %{code: code, message: message, data: %{block_number: block_number}}}` - some beneficiaries were successfully fetched and some may have had errors. * `{:ok, %EthereumJSONRPC.FetchedBeneficiaries{params_set: [%{address_hash: address_hash, block_number: block_number}], errors: %{code: code, message: message, data: %{block_number: block_number}}}` - some beneficiaries were successfully fetched and some may have had errors.
* `{:error, reason}` - there was an error at the transport level * `{:error, reason}` - there was an error at the transport level
* `:ignore` - the variant does not support fetching beneficiaries * `:ignore` - the variant does not support fetching beneficiaries
""" """

@ -1,8 +1,8 @@
defmodule EthereumJSONRPC.Nethermind.FetchedBeneficiariesTest do defmodule EthereumJSONRPC.FetchedBeneficiariesTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias EthereumJSONRPC alias EthereumJSONRPC
alias EthereumJSONRPC.Nethermind.FetchedBeneficiaries alias EthereumJSONRPC.FetchedBeneficiaries
describe "from_responses/2" do describe "from_responses/2" do
test "when block is not found" do test "when block is not found" do

@ -165,6 +165,9 @@ config :ethereum_jsonrpc,
debug_trace_transaction_timeout = System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT", "5s") debug_trace_transaction_timeout = System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT", "5s")
config :ethereum_jsonrpc, EthereumJSONRPC.Geth, debug_trace_transaction_timeout: debug_trace_transaction_timeout config :ethereum_jsonrpc, EthereumJSONRPC.Geth, debug_trace_transaction_timeout: debug_trace_transaction_timeout
config :ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction,
type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default")
################ ################
### Explorer ### ### Explorer ###
################ ################

Loading…
Cancel
Save