diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex index 2e5eaeff32..198ba61be4 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu.ex @@ -5,13 +5,26 @@ defmodule EthereumJSONRPC.Besu do require Logger import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] - alias EthereumJSONRPC.Parity.{Traces} + alias EthereumJSONRPC.Parity.{FetchedBeneficiaries, Traces} alias EthereumJSONRPC.{Transaction, Transactions} @behaviour EthereumJSONRPC.Variant @impl EthereumJSONRPC.Variant - def fetch_beneficiaries(_block_range, _json_rpc_named_arguments), do: :ignore + def fetch_beneficiaries(block_numbers, json_rpc_named_arguments) + when is_list(block_numbers) and is_list(json_rpc_named_arguments) do + id_to_params = + block_numbers + |> block_numbers_to_params_list() + |> id_to_params() + + with {:ok, responses} <- + id_to_params + |> FetchedBeneficiaries.requests() + |> json_rpc(json_rpc_named_arguments) do + {:ok, FetchedBeneficiaries.from_responses(responses, id_to_params)} + end + end @impl EthereumJSONRPC.Variant def fetch_internal_transactions(_transactions_params, _json_rpc_named_arguments), do: :ignore @@ -88,6 +101,10 @@ defmodule EthereumJSONRPC.Besu do 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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex new file mode 100644 index 0000000000..13638272d0 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/besu/fetched_beneficiaries.ex @@ -0,0 +1,197 @@ +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 == "external" and index == 3, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 4, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 5, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 6, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 7, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 8, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 9, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 10, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 11, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 12, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 13, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 14, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 15, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 16, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 17, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 18, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 19, do: :validator + defp get_address_type(reward_type, index) when reward_type == "external" and index == 20, 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 + \ No newline at end of file