diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 8e1b7519e3..4addc36998 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -29,6 +29,7 @@ defmodule EthereumJSONRPC do Block, Blocks, FetchedBalances, + FetchedCodes, Receipts, RequestCoordinator, Subscription, @@ -54,6 +55,11 @@ defmodule EthereumJSONRPC do """ @type data :: String.t() + @typedoc """ + Contract code encoded as a single hexadecimal number in a `String.t` + """ + @type code :: String.t() + @typedoc """ A full 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a `String.t` @@ -201,6 +207,25 @@ defmodule EthereumJSONRPC do end end + @doc """ + Fetches code for each given `address` at the `block_number`. + """ + @spec fetch_codes( + [%{required(:block_quantity) => quantity, required(:address) => address()}], + json_rpc_named_arguments + ) :: {:ok, FetchedCodes.t()} | {:error, reason :: term} + def fetch_codes(params_list, json_rpc_named_arguments) + when is_list(params_list) and is_list(json_rpc_named_arguments) do + id_to_params = id_to_params(params_list) + + with {:ok, responses} <- + id_to_params + |> FetchedCodes.requests() + |> json_rpc(json_rpc_named_arguments) do + {:ok, FetchedCodes.from_responses(responses, id_to_params)} + end + end + @doc """ Fetches block reward contract beneficiaries from variant API. """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_code.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_code.ex new file mode 100644 index 0000000000..58d52509b4 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_code.ex @@ -0,0 +1,50 @@ +defmodule EthereumJSONRPC.FetchedCode do + @moduledoc """ + A single code fetched from `eth_getCode`. + """ + + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + @type params :: %{address: EthereumJSONRPC.address(), block_number: non_neg_integer(), code: non_neg_integer()} + @type error :: %{code: integer(), message: String.t(), data: %{block_quantity: String.t(), address: String.t()}} + + @doc """ + Converts `response` to code params or annotated error. + """ + + def from_response(%{id: id, result: fetched_code}, id_to_params) when is_map(id_to_params) do + %{block_quantity: block_quantity, address: address} = Map.fetch!(id_to_params, id) + + {:ok, + %{ + address: address, + block_number: quantity_to_integer(block_quantity), + code: fetched_code + }} + end + + @spec from_response(%{id: id, result: String.t()}, %{id => %{block_quantity: block_quantity, address: address}}) :: + {:ok, params()} + when id: non_neg_integer(), block_quantity: String.t(), address: String.t() + def from_response(%{id: id, error: %{code: code, message: message} = error}, id_to_params) + when is_integer(code) and is_binary(message) and is_map(id_to_params) do + %{block_quantity: block_quantity, address: address} = Map.fetch!(id_to_params, id) + + annotated_error = Map.put(error, :data, %{block_quantity: block_quantity, address: address}) + + {:error, annotated_error} + end + + @spec request(%{id: id, block_quantity: block_quantity, address: address}) :: %{ + jsonrpc: String.t(), + id: id, + method: String.t(), + params: [address | block_quantity] + } + when id: EthereumJSONRPC.request_id(), + block_quantity: EthereumJSONRPC.quantity(), + address: EthereumJSONRPC.address() + def request(%{id: id, block_quantity: block_quantity, address: address}) do + EthereumJSONRPC.request(%{id: id, method: "eth_getCode", params: [address, block_quantity]}) + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_codes.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_codes.ex new file mode 100644 index 0000000000..8369285194 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/fetched_codes.ex @@ -0,0 +1,49 @@ +defmodule EthereumJSONRPC.FetchedCodes do + @moduledoc """ + Code params and errors from a batch request from `eth_getCode`. + """ + + alias EthereumJSONRPC.FetchedCode + + defstruct params_list: [], + errors: [] + + @typedoc """ + * `params_list` - all the code params from requests that succeeded in the batch. + * `errors` - all the errors from requests that failed in the batch. + """ + @type t :: %__MODULE__{params_list: [FetchedCode.params()], errors: [FetchedCode.error()]} + + @doc """ + `eth_getCode` requests for `id_to_params`. + """ + @spec requests(%{id => %{block_quantity: block_quantity, address: address}}) :: [ + %{jsonrpc: String.t(), id: id, method: String.t(), params: [address | block_quantity]} + ] + when id: EthereumJSONRPC.request_id(), + block_quantity: EthereumJSONRPC.quantity(), + address: EthereumJSONRPC.address() + def requests(id_to_params) when is_map(id_to_params) do + Enum.map(id_to_params, fn {id, %{block_quantity: block_quantity, address: address}} -> + FetchedCode.request(%{id: id, block_quantity: block_quantity, address: address}) + end) + end + + @doc """ + Converts `responses` to `t/0`. + """ + def from_responses(responses, id_to_params) do + responses + |> Enum.map(&FetchedCode.from_response(&1, id_to_params)) + |> Enum.reduce( + %__MODULE__{}, + fn + {:ok, params}, %__MODULE__{params_list: params_list} = acc -> + %__MODULE__{acc | params_list: [params | params_list]} + + {:error, reason}, %__MODULE__{errors: errors} = acc -> + %__MODULE__{acc | errors: [reason | errors]} + end + ) + end +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs index 1f99bd8697..849acb7f3e 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs @@ -4,7 +4,7 @@ defmodule EthereumJSONRPCTest do import EthereumJSONRPC.Case import Mox - alias EthereumJSONRPC.{Blocks, FetchedBalances, FetchedBeneficiaries, Subscription} + alias EthereumJSONRPC.{Blocks, FetchedBalances, FetchedBeneficiaries, FetchedCodes, Subscription} alias EthereumJSONRPC.WebSocket.WebSocketClient setup :verify_on_exit! @@ -173,6 +173,86 @@ defmodule EthereumJSONRPCTest do end end + describe "fetch_balances/2" do + test "returns both codes and errors", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options -> + { + :ok, + [ + %{ + id: 0, + result: + "0x600160008035811a818181146012578301005b601b6001356025565b8060005260206000f25b600060078202905091905056" + }, + %{ + id: 1, + error: %{ + code: -32602, + message: + "Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." + } + }, + %{ + id: 2, + result: + "0x7009600160008035811a818181146012578301005b601b6001356025565b8060005260206000f25b600060078202905091905" + } + ] + } + end) + end + + assert {:ok, %FetchedCodes{params_list: params_list, errors: errors}} = + EthereumJSONRPC.fetch_codes( + [ + # start with :ok + %{ + block_quantity: "0x1", + address: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + }, + # :ok, :error clause + %{ + block_quantity: "0x2", + address: "0x3" + }, + # :error, :ok clause + %{ + block_quantity: "0x35", + address: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + ], + json_rpc_named_arguments + ) + + assert params_list == [ + %{ + address: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + block_number: 53, + code: + "0x7009600160008035811a818181146012578301005b601b6001356025565b8060005260206000f25b600060078202905091905" + }, + %{ + address: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + block_number: 1, + code: + "0x600160008035811a818181146012578301005b601b6001356025565b8060005260206000f25b600060078202905091905056" + } + ] + + assert errors == [ + %{ + code: -32602, + data: %{address: "0x3", block_quantity: "0x2"}, + message: + "Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." + } + ] + end + end + describe "fetch_beneficiaries/2" do @tag :no_geth test "fetches benefeciaries from variant API", %{json_rpc_named_arguments: json_rpc_named_arguments} do