diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ec6251be3..4f5c32dd2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 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" - [#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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex index 29707f0a33..67b38e85c8 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex @@ -3,11 +3,10 @@ defmodule EthereumJSONRPC.Besu do @moduledoc """ 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, request: 1] + import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2] - alias EthereumJSONRPC.Besu.{FetchedBeneficiaries, Traces} - alias EthereumJSONRPC.{Transaction, Transactions} + alias EthereumJSONRPC.Besu.Traces + alias EthereumJSONRPC.{FetchedBeneficiaries, PendingTransaction, TraceReplayBlockTransactions, Transaction} @behaviour EthereumJSONRPC.Variant @@ -35,48 +34,12 @@ defmodule EthereumJSONRPC.Besu do """ @impl EthereumJSONRPC.Variant 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) - - 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 + TraceReplayBlockTransactions.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, Traces) end @impl EthereumJSONRPC.Variant def fetch_first_trace(transactions_params, json_rpc_named_arguments) 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) 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 + TraceReplayBlockTransactions.fetch_first_trace(transactions_params, json_rpc_named_arguments, Traces) end @doc """ @@ -89,217 +52,10 @@ defmodule EthereumJSONRPC.Besu do @spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) :: {:ok, [Transaction.params()]} | {:error, reason :: term} def fetch_pending_transactions(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 + PendingTransaction.fetch_pending_transactions_besu(json_rpc_named_arguments) end defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) 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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex deleted file mode 100644 index 0cc22c19f8..0000000000 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex +++ /dev/null @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/erigon.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/erigon.ex index 422e6ccef5..8173a98df2 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/erigon.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/erigon.ex @@ -3,11 +3,10 @@ defmodule EthereumJSONRPC.Erigon do @moduledoc """ 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, request: 1] + import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2] - alias EthereumJSONRPC.Nethermind.{FetchedBeneficiaries, Traces} - alias EthereumJSONRPC.Transactions + alias EthereumJSONRPC.Nethermind.Traces + alias EthereumJSONRPC.{FetchedBeneficiaries, PendingTransaction, TraceReplayBlockTransactions} @behaviour EthereumJSONRPC.Variant @@ -35,48 +34,12 @@ defmodule EthereumJSONRPC.Erigon do """ @impl EthereumJSONRPC.Variant 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) - - 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 + TraceReplayBlockTransactions.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, Traces) end @impl EthereumJSONRPC.Variant def fetch_first_trace(transactions_params, json_rpc_named_arguments) 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) 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 + TraceReplayBlockTransactions.fetch_first_trace(transactions_params, json_rpc_named_arguments, Traces) end @doc """ @@ -84,227 +47,10 @@ defmodule EthereumJSONRPC.Erigon do """ @impl EthereumJSONRPC.Variant def fetch_pending_transactions(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 - |> Map.merge(%{:block_hash => nil, :index => nil}) - end) - - {:ok, transactions_params} - end + PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments) end defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) 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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_beneficiaries.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_beneficiaries.ex index 8dcf2cd8b6..4e55e6e2a2 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_beneficiaries.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_beneficiaries.ex @@ -1,8 +1,10 @@ defmodule EthereumJSONRPC.FetchedBeneficiaries do @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 defstruct params_set: MapSet.new(), @@ -13,4 +15,181 @@ defmodule EthereumJSONRPC.FetchedBeneficiaries do * `errors` - all the errors from requests that failed in the batch. """ @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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index b067ed1f1c..c14f216035 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -5,7 +5,7 @@ defmodule EthereumJSONRPC.Geth do 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} @behaviour EthereumJSONRPC.Variant @@ -56,26 +56,7 @@ defmodule EthereumJSONRPC.Geth do """ @impl EthereumJSONRPC.Variant def fetch_pending_transactions(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 + PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments) end defp debug_trace_transaction_requests(id_to_params) when is_map(id_to_params) do diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind.ex index fa6dbb176e..1359e97f39 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind.ex @@ -3,11 +3,10 @@ defmodule EthereumJSONRPC.Nethermind do @moduledoc """ 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, request: 1] + import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2] - alias EthereumJSONRPC.Nethermind.{FetchedBeneficiaries, Traces} - alias EthereumJSONRPC.{Transaction, Transactions} + alias EthereumJSONRPC.Nethermind.Traces + alias EthereumJSONRPC.{FetchedBeneficiaries, PendingTransaction, TraceReplayBlockTransactions, Transaction} @behaviour EthereumJSONRPC.Variant @@ -35,48 +34,12 @@ defmodule EthereumJSONRPC.Nethermind do """ @impl EthereumJSONRPC.Variant 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) - - 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 + TraceReplayBlockTransactions.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments, Traces) end @impl EthereumJSONRPC.Variant def fetch_first_trace(transactions_params, json_rpc_named_arguments) 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) 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 + TraceReplayBlockTransactions.fetch_first_trace(transactions_params, json_rpc_named_arguments, Traces) end @doc """ @@ -89,217 +52,14 @@ defmodule EthereumJSONRPC.Nethermind do @spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) :: {:ok, [Transaction.params()]} | {:error, reason :: term} def fetch_pending_transactions(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} + if Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction)[:type] == "geth" do + PendingTransaction.fetch_pending_transactions_geth(json_rpc_named_arguments) + else + PendingTransaction.fetch_pending_transactions_parity(json_rpc_named_arguments) end end defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) 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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex similarity index 100% rename from apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace.ex rename to apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace/action.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace/action.ex similarity index 100% rename from apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace/action.ex rename to apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace/action.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace/result.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace/result.ex similarity index 100% rename from apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/trace/result.ex rename to apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace/result.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/traces.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/traces.ex similarity index 100% rename from apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/traces.ex rename to apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/traces.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex deleted file mode 100644 index b1d6222a33..0000000000 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex +++ /dev/null @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex new file mode 100644 index 0000000000..b2754163f0 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex new file mode 100644 index 0000000000..49b395b86b --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex index 3bcb199224..7dcff89363 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex @@ -22,7 +22,7 @@ defmodule EthereumJSONRPC.Variant do ## 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 * `:ignore` - the variant does not support fetching beneficiaries """ diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/fetched_beneficiaries_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/fetched_beneficiaries_test.exs similarity index 98% rename from apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/fetched_beneficiaries_test.exs rename to apps/ethereum_jsonrpc/test/ethereum_jsonrpc/fetched_beneficiaries_test.exs index c9b29ff99c..b2450f2d6d 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/fetched_beneficiaries_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/fetched_beneficiaries_test.exs @@ -1,8 +1,8 @@ -defmodule EthereumJSONRPC.Nethermind.FetchedBeneficiariesTest do +defmodule EthereumJSONRPC.FetchedBeneficiariesTest do use ExUnit.Case, async: true alias EthereumJSONRPC - alias EthereumJSONRPC.Nethermind.FetchedBeneficiaries + alias EthereumJSONRPC.FetchedBeneficiaries describe "from_responses/2" do test "when block is not found" do diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace/action_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace/action_test.exs similarity index 100% rename from apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace/action_test.exs rename to apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace/action_test.exs diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace/result_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace/result_test.exs similarity index 100% rename from apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace/result_test.exs rename to apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace/result_test.exs diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace_test.exs similarity index 100% rename from apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity/trace_test.exs rename to apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind/trace_test.exs diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/parity/http_websocket.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex similarity index 100% rename from apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/parity/http_websocket.ex rename to apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/parity/mox.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/mox.ex similarity index 100% rename from apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/parity/mox.ex rename to apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/mox.ex diff --git a/config/runtime.exs b/config/runtime.exs index f661efbe40..1f2bf922a6 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -165,6 +165,9 @@ config :ethereum_jsonrpc, 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.PendingTransaction, + type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default") + ################ ### Explorer ### ################