diff --git a/.dialyzer-ignore b/.dialyzer-ignore index 193d2b7917..c0a7b3009b 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -1,6 +1,6 @@ :0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3 :0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2 :0: Unknown type 'Elixir.Map':t/0 -apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:413: Function timestamp_to_datetime/1 has no local return +apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:390: Function timestamp_to_datetime/1 has no local return apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer() \ No newline at end of file diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 9b5ea02d35..28cf47ed95 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -28,6 +28,7 @@ defmodule EthereumJSONRPC do alias EthereumJSONRPC.{ Block, Blocks, + Contract, FetchedBalances, FetchedBeneficiaries, FetchedCodes, @@ -160,33 +161,9 @@ defmodule EthereumJSONRPC do } ]} """ - @spec execute_contract_functions( - [%{contract_address: String.t(), data: String.t(), id: String.t()}], - json_rpc_named_arguments, - [{:block_number, non_neg_integer()}] - ) :: {:ok, list()} | {:error, term()} - def execute_contract_functions(functions, json_rpc_named_arguments, opts \\ []) do - block_number = Keyword.get(opts, :block_number) - - functions - |> Enum.map(&build_eth_call_payload(&1, block_number)) - |> json_rpc(json_rpc_named_arguments) - end - - defp build_eth_call_payload( - %{contract_address: address, data: data, id: id}, - nil = _block_number - ) do - params = [%{to: address, data: data}, "latest"] - request(%{id: id, method: "eth_call", params: params}) - end - - defp build_eth_call_payload( - %{contract_address: address, data: data, id: id}, - block_number - ) do - params = [%{to: address, data: data}, integer_to_quantity(block_number)] - request(%{id: id, method: "eth_call", params: params}) + @spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()] + def execute_contract_functions(functions, abi, json_rpc_named_arguments) do + Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments) end @doc """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex new file mode 100644 index 0000000000..57010769c0 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex @@ -0,0 +1,94 @@ +defmodule EthereumJSONRPC.Contract do + @moduledoc """ + Smart contract functions executed by `eth_call`. + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1, json_rpc: 2, request: 1] + + alias EthereumJSONRPC.Encoder + + @typedoc """ + Call to a smart contract function. + + * `:block_number` - the block in which to execute the function. Defaults to the `nil` to indicate + the latest block as determined by the remote node, which may differ from the latest block number + in `Explorer.Chain`. + """ + @type call :: %{ + required(:contract_address) => String.t(), + required(:function_name) => String.t(), + required(:args) => [term()], + optional(:block_number) => EthereumJSONRPC.block_number() + } + + @typedoc """ + Result of calling a smart contract function. + """ + @type call_result :: {:ok, term()} | {:error, String.t()} + + @spec execute_contract_functions([call()], [map()], EthereumJSONRPC.json_rpc_named_arguments()) :: [call_result()] + def execute_contract_functions(requests, abi, json_rpc_named_arguments) do + functions = + abi + |> ABI.parse_specification() + |> Enum.into(%{}, &{&1.function, &1}) + + requests_with_index = Enum.with_index(requests) + + indexed_responses = + requests_with_index + |> Enum.map(fn {%{contract_address: contract_address, function_name: function_name, args: args} = request, index} -> + functions[function_name] + |> Encoder.encode_function_call(args) + |> eth_call_request(contract_address, index, Map.get(request, :block_number)) + end) + |> json_rpc(json_rpc_named_arguments) + |> case do + {:ok, responses} -> responses + {:error, {:bad_gateway, _request_url}} -> raise "Bad gateway" + {:error, error} -> raise error + end + |> Enum.into(%{}, &{&1.id, &1}) + + Enum.map(requests_with_index, fn {%{function_name: function_name}, index} -> + indexed_responses[index] + |> case do + nil -> + {:error, "No result"} + + response -> + {^index, result} = Encoder.decode_result(response, functions[function_name]) + result + end + end) + rescue + error -> + Enum.map(requests, fn _ -> format_error(error) end) + end + + defp eth_call_request(data, contract_address, id, block_number) do + block = + case block_number do + nil -> "latest" + block_number -> integer_to_quantity(block_number) + end + + request(%{ + id: id, + method: "eth_call", + params: [%{to: contract_address, data: data}, block] + }) + end + + defp format_error(message) when is_binary(message) do + {:error, message} + end + + defp format_error(%{message: error_message}) do + format_error(error_message) + end + + defp format_error(error) do + format_error(Exception.message(error)) + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex index 1ce8190810..c43737cfdb 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex @@ -6,49 +6,19 @@ defmodule EthereumJSONRPC.Encoder do alias ABI.TypeDecoder - @doc """ - Given an ABI and a set of functions, returns the data the blockchain expects. - """ - @spec encode_abi([map()], %{String.t() => [any()]}) :: map() - def encode_abi(abi, functions) do - abi - |> ABI.parse_specification() - |> get_selectors(functions) - |> Enum.map(&encode_function_call/1) - |> Map.new() - end - - @doc """ - Given a list of function selectors from the ABI lib, and a list of functions names with their arguments, returns a list of selectors with their functions. - """ - @spec get_selectors([%ABI.FunctionSelector{}], %{String.t() => [term()]}) :: [{%ABI.FunctionSelector{}, [term()]}] - def get_selectors(abi, functions) do - Enum.map(functions, fn {function_name, args} -> - {get_selector_from_name(abi, function_name), args} - end) - end - - @doc """ - Given a list of function selectors from the ABI lib, and a function name, get the selector for that function. - """ - @spec get_selector_from_name([%ABI.FunctionSelector{}], String.t()) :: %ABI.FunctionSelector{} - def get_selector_from_name(abi, function_name) do - Enum.find(abi, fn selector -> function_name == selector.function end) - end - @doc """ Given a function selector and a list of arguments, returns their encoded versions. This is what is expected on the Json RPC data parameter. """ - @spec encode_function_call({%ABI.FunctionSelector{}, [term()]}) :: {String.t(), String.t()} - def encode_function_call({function_selector, args}) do + @spec encode_function_call(%ABI.FunctionSelector{}, [term()]) :: String.t() + def encode_function_call(function_selector, args) do encoded_args = function_selector |> ABI.encode(parse_args(args)) |> Base.encode16(case: :lower) - {function_selector.function, "0x" <> encoded_args} + "0x" <> encoded_args end defp parse_args(args) do @@ -62,39 +32,16 @@ defmodule EthereumJSONRPC.Encoder do end) end - @doc """ - Given a result set from the blockchain, and the functions selectors, returns the results decoded. - - This functions assumes the result["id"] is the name of the function the result is for. - """ - @spec decode_abi_results([map()], [map()], %{String.t() => [any()]}) :: map() - def decode_abi_results(results, abi, functions) do - selectors = - abi - |> ABI.parse_specification() - |> get_selectors(functions) - |> Enum.map(fn {selector, _args} -> selector end) - - results - |> Stream.map(&join_result_and_selector(&1, selectors)) - |> Stream.map(&decode_result/1) - |> Map.new() - end - - defp join_result_and_selector(result, selectors) do - {result, Enum.find(selectors, &(&1.function == result[:id]))} - end - @doc """ Given a result from the blockchain, and the function selector, returns the result decoded. """ - @spec decode_result({map(), %ABI.FunctionSelector{}}) :: + @spec decode_result(map(), %ABI.FunctionSelector{}) :: {String.t(), {:ok, any()} | {:error, String.t() | :invalid_data}} - def decode_result({%{error: %{code: code, message: message}, id: id}, _selector}) do + def decode_result(%{error: %{code: code, message: message}, id: id}, _selector) do {id, {:error, "(#{code}) #{message}"}} end - def decode_result({%{id: id, result: result}, function_selector}) do + def decode_result(%{id: id, result: result}, function_selector) do types_list = List.wrap(function_selector.returns) decoded_data = diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs new file mode 100644 index 0000000000..6131d162e5 --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs @@ -0,0 +1,134 @@ +defmodule EthereumJSONRPC.ContractTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Contract + + import Mox + + describe "execute_contract_functions/3" do + test "executes the functions with and without the block_number, returns results in order" do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + abi = [ + %{ + "constant" => false, + "inputs" => [], + "name" => "get1", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get2", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get3", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + + contract_address = "0x0000000000000000000000000000000000000000" + + functions = [ + %{contract_address: contract_address, function_name: "get1", args: []}, + %{contract_address: contract_address, function_name: "get2", args: [], block_number: 1000}, + %{contract_address: contract_address, function_name: "get3", args: []} + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn requests, _options -> + {:ok, + requests + |> Enum.map(fn + %{id: id, method: "eth_call", params: [%{data: "0x054c1a75", to: ^contract_address}, "latest"]} -> + %{ + id: id, + result: "0x000000000000000000000000000000000000000000000000000000000000002a" + } + + %{id: id, method: "eth_call", params: [%{data: "0xd2178b08", to: ^contract_address}, "0x3E8"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000034" + } + + %{id: id, method: "eth_call", params: [%{data: "0x8321045c", to: ^contract_address}, "latest"]} -> + %{ + id: id, + error: %{code: -32015, data: "something", message: "Some error"} + } + end) + |> Enum.shuffle()} + end + ) + + blockchain_result = [ + {:ok, [42]}, + {:ok, [52]}, + {:error, "(-32015) Some error"} + ] + + assert EthereumJSONRPC.execute_contract_functions( + functions, + abi, + json_rpc_named_arguments + ) == blockchain_result + end + + test "returns errors if JSONRPC request fails" do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + abi = [ + %{ + "constant" => false, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ] + + contract_address = "0x0000000000000000000000000000000000000000" + + functions = [ + %{contract_address: contract_address, function_name: "get", args: []}, + %{contract_address: contract_address, function_name: "get", args: [], block_number: 1000} + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn _requests, _options -> + {:error, "Some error"} + end + ) + + blockchain_result = [ + {:error, "Some error"}, + {:error, "Some error"} + ] + + assert EthereumJSONRPC.execute_contract_functions( + functions, + abi, + json_rpc_named_arguments + ) == blockchain_result + end + end +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs index 562df009eb..01fe9036de 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs @@ -13,7 +13,7 @@ defmodule EthereumJSONRPC.EncoderTest do types: [] } - assert Encoder.encode_function_call({function_selector, []}) == {"get", "0x6d4ce63c"} + assert Encoder.encode_function_call(function_selector, []) == "0x6d4ce63c" end test "generates the correct encoding with arguments" do @@ -23,8 +23,8 @@ defmodule EthereumJSONRPC.EncoderTest do types: [{:uint, 256}] } - assert Encoder.encode_function_call({function_selector, [10]}) == - {"get", "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"} + assert Encoder.encode_function_call(function_selector, [10]) == + "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a" end test "generates the correct encoding with addresses arguments" do @@ -36,127 +36,12 @@ defmodule EthereumJSONRPC.EncoderTest do args = ["0xdab1c67232f92b7707f49c08047b96a4db7a9fc6", "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"] - assert Encoder.encode_function_call({function_selector, args}) == - {"tokens", - "0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493"} + assert Encoder.encode_function_call(function_selector, args) == + "0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493" end end - describe "get_selectors/2" do - test "return the selectors of the desired functions with their arguments" do - abi = [ - %ABI.FunctionSelector{ - function: "fn1", - returns: {:uint, 256}, - types: [uint: 256] - }, - %ABI.FunctionSelector{ - function: "fn2", - returns: {:uint, 256}, - types: [uint: 256] - } - ] - - fn1 = %ABI.FunctionSelector{ - function: "fn1", - returns: {:uint, 256}, - types: [uint: 256] - } - - assert Encoder.get_selectors(abi, %{"fn1" => [10]}) == [{fn1, [10]}] - end - end - - describe "get_selector_from_name/2" do - test "return the selector of the desired function" do - abi = [ - %ABI.FunctionSelector{ - function: "fn1", - returns: {:uint, 256}, - types: [uint: 256] - }, - %ABI.FunctionSelector{ - function: "fn2", - returns: {:uint, 256}, - types: [uint: 256] - } - ] - - fn1 = %ABI.FunctionSelector{ - function: "fn1", - returns: {:uint, 256}, - types: [uint: 256] - } - - assert Encoder.get_selector_from_name(abi, "fn1") == fn1 - end - end - - describe "decode_abi_results/3" do - test "separates the selectors and map the results" do - result = [ - %{ - id: "get1", - jsonrpc: "2.0", - result: "0x000000000000000000000000000000000000000000000000000000000000002a" - }, - %{ - id: "get2", - jsonrpc: "2.0", - result: "0x000000000000000000000000000000000000000000000000000000000000002a" - }, - %{ - id: "get3", - jsonrpc: "2.0", - result: "0x0000000000000000000000000000000000000000000000000000000000000020" - } - ] - - abi = [ - %{ - "constant" => false, - "inputs" => [], - "name" => "get1", - "outputs" => [%{"name" => "", "type" => "uint256"}], - "payable" => false, - "stateMutability" => "nonpayable", - "type" => "function" - }, - %{ - "constant" => true, - "inputs" => [], - "name" => "get2", - "outputs" => [%{"name" => "", "type" => "uint256"}], - "payable" => false, - "stateMutability" => "view", - "type" => "function" - }, - %{ - "constant" => true, - "inputs" => [], - "name" => "get3", - "outputs" => [%{"name" => "", "type" => "uint256"}], - "payable" => false, - "stateMutability" => "view", - "type" => "function" - } - ] - - functions = %{ - "get1" => [], - "get2" => [], - "get3" => [] - } - - assert Encoder.decode_abi_results(result, abi, functions) == %{ - "get1" => {:ok, [42]}, - "get2" => {:ok, [42]}, - "get3" => {:ok, [32]} - } - end - end - - describe "decode_result/1" do + describe "decode_result/2" do test "correctly decodes the blockchain result" do result = %{ id: "sum", @@ -170,7 +55,7 @@ defmodule EthereumJSONRPC.EncoderTest do types: [{:uint, 256}] } - assert Encoder.decode_result({result, selector}) == {"sum", {:ok, [42]}} + assert Encoder.decode_result(result, selector) == {"sum", {:ok, [42]}} end test "correctly handles the blockchain error response" do @@ -189,7 +74,7 @@ defmodule EthereumJSONRPC.EncoderTest do types: [{:uint, 256}] } - assert Encoder.decode_result({result, selector}) == + assert Encoder.decode_result(result, selector) == {"sum", {:error, "(-32602) Invalid params: Invalid hex: Invalid character 'x' at position 134."}} end @@ -199,7 +84,7 @@ defmodule EthereumJSONRPC.EncoderTest do selector = %ABI.FunctionSelector{function: "name", types: [], returns: [:string]} - assert Encoder.decode_result({%{id: "storedName", result: result}, selector}) == {"storedName", {:ok, ["AION"]}} + assert Encoder.decode_result(%{id: "storedName", result: result}, selector) == {"storedName", {:ok, ["AION"]}} end end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs index 54bd766028..5f82b6a43f 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs @@ -886,93 +886,6 @@ defmodule EthereumJSONRPCTest do end end - describe "execute_contract_functions/3" do - test "executes the functions with the block_number" do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - functions = [ - %{ - contract_address: "0x0000000000000000000000000000000000000000", - data: "0x6d4ce63c", - id: "get" - } - ] - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x0000000000000000000000000000000000000000000000000000000000000000" - } - ]} - end - ) - - blockchain_result = - {:ok, - [ - %{ - id: "get", - jsonrpc: "2.0", - result: "0x0000000000000000000000000000000000000000000000000000000000000000" - } - ]} - - assert EthereumJSONRPC.execute_contract_functions( - functions, - json_rpc_named_arguments, - block_number: 1000 - ) == blockchain_result - end - - test "executes the functions even when the block_number is not given" do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - functions = [ - %{ - contract_address: "0x0000000000000000000000000000000000000000", - data: "0x6d4ce63c", - id: "get" - } - ] - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, "latest"]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x0000000000000000000000000000000000000000000000000000000000000000" - } - ]} - end - ) - - blockchain_result = - {:ok, - [ - %{ - id: "get", - jsonrpc: "2.0", - result: "0x0000000000000000000000000000000000000000000000000000000000000000" - } - ]} - - assert EthereumJSONRPC.execute_contract_functions( - functions, - json_rpc_named_arguments - ) == blockchain_result - end - end - defp clear_mailbox do receive do _ -> clear_mailbox() diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 3f20119eee..72b40f55a4 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -6,9 +6,9 @@ defmodule Explorer.SmartContract.Reader do [wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI). """ - alias EthereumJSONRPC.Encoder + alias EthereumJSONRPC.Contract alias Explorer.Chain - alias Explorer.Chain.{Block, Hash} + alias Explorer.Chain.Hash @typedoc """ Map of functions to call with the values for the function to be called with. @@ -18,24 +18,17 @@ defmodule Explorer.SmartContract.Reader do @typedoc """ Map of function call to function call results. """ - @type functions_results :: %{String.t() => {:ok, term()} | {:error, String.t()}} + @type functions_results :: %{String.t() => Contract.call_result()} @typedoc """ Options that can be forwarded when calling the Ethereum JSON RPC. - ## Required - - * `:json_rpc_named_arguments` - the named arguments to `EthereumJSONRPC.json_rpc/2`. - ## Optional - * `:block_number` - the block in which to execute the function. Defaults to the `nil` to indicate - the latest block as determined by the remote node, which may differ from the latest block number - in `Explorer.Chain`. + * `:json_rpc_named_arguments` - the named arguments to `EthereumJSONRPC.json_rpc/2`. """ @type contract_call_options :: [ - {:json_rpc_named_arguments, EthereumJSONRPC.json_rpc_named_arguments()}, - {:block_number, Block.block_number()} + {:json_rpc_named_arguments, EthereumJSONRPC.json_rpc_named_arguments()} ] @doc """ @@ -90,55 +83,44 @@ defmodule Explorer.SmartContract.Reader do @spec query_contract( String.t(), term(), - functions(), - contract_call_options() + functions() ) :: functions_results() - def query_contract(contract_address, abi, functions, opts \\ []) do - json_rpc_named_arguments = - Keyword.get(opts, :json_rpc_named_arguments) || Application.get_env(:explorer, :json_rpc_named_arguments) - - abi - |> Encoder.encode_abi(functions) - |> Enum.map(&setup_call_payload(&1, contract_address)) - |> EthereumJSONRPC.execute_contract_functions(json_rpc_named_arguments, opts) - |> decode_results(abi, functions) - rescue - error -> - format_error(functions, error) - end - - defp decode_results({:ok, results}, abi, functions), do: Encoder.decode_abi_results(results, abi, functions) - - defp decode_results({:error, {:bad_gateway, _request_url}}, _abi, functions) do - format_error(functions, "Bad Gateway") - end + def query_contract(contract_address, abi, functions) do + requests = + functions + |> Enum.map(fn {function_name, args} -> + %{ + contract_address: contract_address, + function_name: function_name, + args: args + } + end) - defp format_error(functions, message) when is_binary(message) do - functions - |> Enum.map(fn {function_name, _args} -> - %{function_name => {:error, message}} + requests + |> query_contracts(abi) + |> Enum.zip(requests) + |> Enum.into(%{}, fn {response, request} -> + {request.function_name, response} end) - |> List.first() end - defp format_error(functions, %{message: error_message}) do - format_error(functions, error_message) - end + @doc """ + Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions. - defp format_error(functions, error) do - format_error(functions, Exception.message(error)) - end + This function can be used to read data from smart contracts that are not verified (like token contracts) + since it receives the ABI as an argument. - @doc """ - Given the encoded data that references a function and its arguments in the blockchain, as well as the contract address, returns what EthereumJSONRPC.execute_contract_functions expects. + ## Options + + * `:json_rpc_named_arguments` - Options to forward for calling the Ethereum JSON RPC. See + `t:EthereumJSONRPC.json_rpc_named_arguments.t/0` for full list of options. """ - @spec setup_call_payload({%ABI.FunctionSelector{}, [term()]}, String.t()) :: map() - def setup_call_payload({function_name, data}, contract_address) do - %{ - contract_address: contract_address, - data: data, - id: function_name - } + @spec query_contracts([Contract.call()], term(), contract_call_options()) :: [Contract.call_result()] + def query_contracts(requests, abi, opts \\ []) do + json_rpc_named_arguments = + Keyword.get(opts, :json_rpc_named_arguments) || Application.get_env(:explorer, :json_rpc_named_arguments) + + EthereumJSONRPC.execute_contract_functions(requests, abi, json_rpc_named_arguments) end @doc """ diff --git a/apps/explorer/lib/explorer/token/balance_reader.ex b/apps/explorer/lib/explorer/token/balance_reader.ex index 41b1b22e58..8403e1a632 100644 --- a/apps/explorer/lib/explorer/token/balance_reader.ex +++ b/apps/explorer/lib/explorer/token/balance_reader.ex @@ -27,26 +27,34 @@ defmodule Explorer.Token.BalanceReader do } ] - @spec get_balance_of(String.t(), String.t(), non_neg_integer()) :: {atom(), non_neg_integer() | String.t()} - def get_balance_of(token_contract_address_hash, address_hash, block_number) do - result = - Reader.query_contract( - token_contract_address_hash, - @balance_function_abi, - %{ - "balanceOf" => [address_hash] - }, - block_number: block_number - ) + @spec get_balances_of([ + %{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()} + ]) :: [{:ok, non_neg_integer()} | {:error, String.t()}] + def get_balances_of(token_balance_requests) do + token_balance_requests + |> Enum.map(&format_balance_request/1) + |> Reader.query_contracts(@balance_function_abi) + |> Enum.map(&format_balance_result/1) + end - format_balance_result(result) + defp format_balance_request(%{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash + }) do + %{ + contract_address: token_contract_address_hash, + function_name: "balanceOf", + args: [address_hash], + block_number: block_number + } end - defp format_balance_result(%{"balanceOf" => {:ok, [balance]}}) do + defp format_balance_result({:ok, [balance]}) do {:ok, balance} end - defp format_balance_result(%{"balanceOf" => {:error, error_message}}) do + defp format_balance_result({:error, error_message}) do {:error, error_message} end end diff --git a/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs b/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs index 07f2b5086e..4b552bc7ba 100644 --- a/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs +++ b/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs @@ -19,7 +19,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do EthereumJSONRPC.Mox |> expect(:json_rpc, fn [ %{ - id: "mintedTotally", + id: id, method: "eth_call", params: [ %{data: "0x553a5c85", to: "0x867305d19606aadba405ce534e303d0e225f9556"}, @@ -31,7 +31,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do {:ok, [ %{ - id: "mintedTotally", + id: id, jsonrpc: "2.0", result: "0x00000000000000000000000000000000000000000000042aa8fe57ebb112dcc8" } @@ -39,7 +39,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do end) |> expect(:json_rpc, fn [ %{ - id: "totalBurntCoins", + id: id, jsonrpc: "2.0", method: "eth_call", params: [ @@ -52,7 +52,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do {:ok, [ %{ - id: "totalBurntCoins", + id: id, jsonrpc: "2.0", result: "0x00000000000000000000000000000000000000000000033cc192839185166fc6" } diff --git a/apps/explorer/test/explorer/smart_contract/reader_test.exs b/apps/explorer/test/explorer/smart_contract/reader_test.exs index 188570607a..ab63b74d1e 100644 --- a/apps/explorer/test/explorer/smart_contract/reader_test.exs +++ b/apps/explorer/test/explorer/smart_contract/reader_test.exs @@ -80,7 +80,7 @@ defmodule Explorer.SmartContract.ReaderTest do response = Reader.query_contract(contract_address_hash, abi, %{"get" => []}) - assert %{"get" => {:error, "Bad Gateway"}} = response + assert %{"get" => {:error, "Bad gateway"}} = response end test "handles other types of errors" do @@ -115,19 +115,6 @@ defmodule Explorer.SmartContract.ReaderTest do end end - describe "setup_call_payload/2" do - test "returns the expected payload" do - function_name = "get" - contract_address = "0x123789abc" - data = "0x6d4ce63c" - - assert Reader.setup_call_payload( - {function_name, data}, - contract_address - ) == %{contract_address: "0x123789abc", data: "0x6d4ce63c", id: "get"} - end - end - describe "read_only_functions/1" do test "fetches the smart contract read only functions with the blockchain value" do smart_contract = diff --git a/apps/explorer/test/explorer/token/balance_reader_test.exs b/apps/explorer/test/explorer/token/balance_reader_test.exs index fc1f1fe010..871c688a78 100644 --- a/apps/explorer/test/explorer/token/balance_reader_test.exs +++ b/apps/explorer/test/explorer/token/balance_reader_test.exs @@ -27,9 +27,16 @@ defmodule Explorer.Token.BalanceReaderTest do get_balance_from_blockchain() - result = BalanceReader.get_balance_of(token_contract_address_hash, address_hash, block_number) - - assert result == {:ok, 1_000_000_000_000_000_000_000_000} + result = + BalanceReader.get_balances_of([ + %{ + token_contract_address_hash: token_contract_address_hash, + address_hash: address_hash, + block_number: block_number + } + ]) + + assert result == [{:ok, 1_000_000_000_000_000_000_000_000}] end test "returns the error message when there is one", %{address: address, token: token} do @@ -39,9 +46,16 @@ defmodule Explorer.Token.BalanceReaderTest do get_balance_from_blockchain_with_error() - result = BalanceReader.get_balance_of(token_contract_address_hash, address_hash, block_number) + result = + BalanceReader.get_balances_of([ + %{ + token_contract_address_hash: token_contract_address_hash, + address_hash: address_hash, + block_number: block_number + } + ]) - assert result == {:error, "(-32015) VM execution error."} + assert result == [{:error, "(-32015) VM execution error."}] end end @@ -49,11 +63,11 @@ defmodule Explorer.Token.BalanceReaderTest do expect( EthereumJSONRPC.Mox, :json_rpc, - fn [%{id: _, method: _, params: _}], _options -> + fn [%{id: id, method: "eth_call", params: _}], _options -> {:ok, [ %{ - id: "balanceOf", + id: id, jsonrpc: "2.0", result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" } @@ -66,12 +80,12 @@ defmodule Explorer.Token.BalanceReaderTest do expect( EthereumJSONRPC.Mox, :json_rpc, - fn [%{id: _, method: _, params: _}], _options -> + fn [%{id: id, method: "eth_call", params: _}], _options -> {:ok, [ %{ error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, - id: "balanceOf", + id: id, jsonrpc: "2.0" } ]} diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs index 0de445e8f7..71b3f5ef2e 100644 --- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs @@ -17,28 +17,35 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" - }, - %{ - id: "symbol", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) @@ -59,29 +66,36 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "decimals", - jsonrpc: "2.0" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" - }, - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "symbol", - jsonrpc: "2.0" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + error: %{code: -32015, data: "something", message: "some error"}, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + error: %{code: -32015, data: "something", message: "some error"}, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) @@ -89,20 +103,23 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 2, - fn [%{id: "decimals"}, %{id: "symbol"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "decimals", - jsonrpc: "2.0" - }, - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "symbol", - jsonrpc: "2.0" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + error: %{code: -32015, data: "something", message: "some error"}, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + error: %{code: -32015, data: "something", message: "some error"}, + jsonrpc: "2.0" + } + end)} end ) @@ -122,28 +139,35 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000" - }, - %{ - id: "symbol", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) @@ -158,45 +182,54 @@ defmodule Explorer.Token.MetadataRetrieverTest do end test "considers the symbol nil when it is an invalid string" do + original = Application.get_env(:explorer, :token_functions_reader_max_retries) + + Application.put_env(:explorer, :token_functions_reader_max_retries, 1) + token = insert(:token, contract_address: build(:contract_address)) expect( EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - %{ - id: "name", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - }, - %{ - id: "symbol", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: _id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + nil + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end) + |> Enum.reject(&is_nil/1)} end ) expected = %{ - name: "BNT", decimals: 18, total_supply: 1_000_000_000_000_000_000, symbol: nil } assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected + + Application.put_env(:explorer, :token_functions_reader_max_retries, original) end test "shortens strings larger than 255 characters" do @@ -209,20 +242,46 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "name", - # this is how the token name would come from the blockchain unshortened. - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010c3c627574746f6e20636c6173733d226e61766261722d746f67676c65722220747970653d22627574746f6e2220646174612d746f67676c653d22636f6c6c617073652220646174612d7461726765743d22236e6176626172537570706f72746564436f6e74656e742220617269612d636f6e74726f6c733d226e6176626172537570706f72746564436f6e74656e742220617269612d657870616e6465643d2266616c73652220617269612d6c6162656c3d223c253d20676574746578742822546f67676c65206e617669676174696f6e222920253e223e203c7370616e20636c6173733d226e61766261722d746f67676c65722d69636f6e223e3c2f7370616e3e203c2f627574746f6e3e0000000000000000000000000000000000000000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010c3c627574746f6e20636c6173733d226e61766261722d746f67676c65722220747970653d22627574746f6e2220646174612d746f67676c653d22636f6c6c617073652220646174612d7461726765743d22236e6176626172537570706f72746564436f6e74656e742220617269612d636f6e74726f6c733d226e6176626172537570706f72746564436f6e74656e742220617269612d657870616e6465643d2266616c73652220617269612d6c6162656c3d223c253d20676574746578742822546f67676c65206e617669676174696f6e222920253e223e203c7370616e20636c6173733d226e61766261722d746f67676c65722d69636f6e223e3c2f7370616e3e203c2f627574746f6e3e0000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) - assert MetadataRetriever.get_functions_of(token.contract_address_hash) == %{name: long_token_name_shortened} + expected = %{ + name: long_token_name_shortened, + decimals: 18, + total_supply: 1_000_000_000_000_000_000, + symbol: "BNT" + } + + assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected end test "retries when some function gave error" do @@ -232,19 +291,35 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "symbol", - jsonrpc: "2.0" - }, - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + error: %{code: -32015, data: "something", message: "some error"}, + id: id, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) @@ -252,19 +327,27 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "symbol"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "symbol", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + end)} end ) - assert MetadataRetriever.get_functions_of(token.contract_address_hash) == %{decimals: 18, symbol: "BNT"} + expected = %{ + name: "Bancor", + symbol: "BNT", + total_supply: 1_000_000_000_000_000_000, + decimals: 18 + } + + assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected end test "retries according to the configured number" do @@ -278,29 +361,36 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "decimals", - jsonrpc: "2.0" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" - }, - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "symbol", - jsonrpc: "2.0" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + error: %{code: -32015, data: "something", message: "some error"}, + id: id, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + error: %{code: -32015, data: "something", message: "some error"}, + id: id, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) @@ -308,20 +398,23 @@ defmodule Explorer.Token.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "symbol"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "decimals", - jsonrpc: "2.0" - }, - %{ - error: %{code: -32015, data: "something", message: "some error"}, - id: "symbol", - jsonrpc: "2.0" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + error: %{code: -32015, data: "something", message: "some error"}, + id: id, + jsonrpc: "2.0" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + error: %{code: -32015, data: "something", message: "some error"}, + id: id, + jsonrpc: "2.0" + } + end)} end ) diff --git a/apps/explorer/test/explorer/validator/metadata_retriever_test.exs b/apps/explorer/test/explorer/validator/metadata_retriever_test.exs index 9ef09e82ab..228331df94 100644 --- a/apps/explorer/test/explorer/validator/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/validator/metadata_retriever_test.exs @@ -32,22 +32,22 @@ defmodule Explorer.Validator.MetadataRetrieverTest do end test "raise error when the first contract call fails" do - contract_request_with_error("getValidators") + contract_request_with_error() assert_raise(MatchError, fn -> MetadataRetriever.fetch_data() end) end test "raise error when a call to the metadatc contract fails" do validators_list_mox_ok() - contract_request_with_error("validators") + contract_request_with_error() assert_raise(MatchError, fn -> MetadataRetriever.fetch_data() end) end end - defp contract_request_with_error(id) do + defp contract_request_with_error() do expect( EthereumJSONRPC.Mox, :json_rpc, - fn [%{id: ^id, method: _, params: _}], _options -> + fn [%{id: id, method: _, params: _}], _options -> {:ok, [ %{ @@ -65,11 +65,11 @@ defmodule Explorer.Validator.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "getValidators"}], _opts -> + fn [%{id: id}], _opts -> {:ok, [ %{ - id: "getValidators", + id: id, jsonrpc: "2.0", result: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" @@ -84,11 +84,11 @@ defmodule Explorer.Validator.MetadataRetrieverTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "validators"}], _opts -> + fn [%{id: id}], _opts -> {:ok, [ %{ - id: "validators", + id: id, jsonrpc: "2.0", result: "0x546573746e616d65000000000000000000000000000000000000000000000000556e69746172696f6e000000000000000000000000000000000000000000000030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140585800000000000000000000000000000000000000000000000000000000000030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003afe130e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058585858585858207374726565742058585858585800000000000000000000000000000000000000000000000000000000000000000000000000000000000000" diff --git a/apps/indexer/lib/indexer/token_balance/fetcher.ex b/apps/indexer/lib/indexer/token_balance/fetcher.ex index 2cc348199f..d1fd0c1bd9 100644 --- a/apps/indexer/lib/indexer/token_balance/fetcher.ex +++ b/apps/indexer/lib/indexer/token_balance/fetcher.ex @@ -7,8 +7,7 @@ defmodule Indexer.TokenBalance.Fetcher do only prepare the params, send they to `Indexer.TokenBalances` and relies on its return. It behaves as a `BufferedTask`, so we can configure the `max_batch_size` and the `max_concurrency` to control how many - token balances will be fetched at the same time. Be aware that, for each token balance the indexer will make a request - to the Smart Contract. + token balances will be fetched at the same time. Also, this module set a `retries_count` for each token balance and increment this number to avoid fetching the ones that always raise errors interacting with the Smart Contract. diff --git a/apps/indexer/lib/indexer/token_balances.ex b/apps/indexer/lib/indexer/token_balances.ex index 4e39997bb0..c3a88a05b1 100644 --- a/apps/indexer/lib/indexer/token_balances.ex +++ b/apps/indexer/lib/indexer/token_balances.ex @@ -12,19 +12,11 @@ defmodule Indexer.TokenBalances do alias Explorer.Token.BalanceReader alias Indexer.{TokenBalance, Tracer} - # The timeout used for each process opened by Task.async_stream/3. Default 15s. - @task_timeout 15000 - - def fetch_token_balances_from_blockchain(token_balances) do - fetch_token_balances_from_blockchain(token_balances, []) - end - @doc """ Fetches TokenBalances from specific Addresses and Blocks in the Blockchain - Every `TokenBalance` is fetched asynchronously, but in case an exception is raised (such as a - timeout) during the RPC call the particular TokenBalance request is ignored and sent to - `TokenBalance.Fetcher` to be fetched again. + In case an exception is raised during the RPC call the particular TokenBalance request + is ignored and sent to `TokenBalance.Fetcher` to be fetched again. ## token_balances @@ -34,21 +26,17 @@ defmodule Indexer.TokenBalances do * `address_hash` - The address_hash that we want to know the balance. * `block_number` - The block number that the address_hash has the balance. """ - def fetch_token_balances_from_blockchain([], _opts), do: {:ok, []} + def fetch_token_balances_from_blockchain([]), do: {:ok, []} @decorate span(tracer: Tracer) - def fetch_token_balances_from_blockchain(token_balances, opts) do + def fetch_token_balances_from_blockchain(token_balances) do Logger.debug("fetching token balances", count: Enum.count(token_balances)) - task_timeout = Keyword.get(opts, :timeout, @task_timeout) - - task_callback = traced_fetch_token_balance_callback(Tracer.current_span()) - requested_token_balances = token_balances - |> Task.async_stream(task_callback, timeout: task_timeout, on_timeout: :kill_task) - |> Stream.map(&format_task_results/1) - |> Enum.filter(&ignore_killed_task/1) + |> BalanceReader.get_balances_of() + |> Stream.zip(token_balances) + |> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end) fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1) @@ -70,35 +58,6 @@ defmodule Indexer.TokenBalances do end) end - defp traced_fetch_token_balance_callback(%Spandex.Span{} = span) do - fn balance -> - try do - Tracer.continue_trace_from_span("traced_fetch_token_balance_callback/1", span) - - fetch_token_balance(balance) - after - Tracer.finish_trace() - end - end - end - - defp traced_fetch_token_balance_callback(_) do - &fetch_token_balance/1 - end - - @decorate span(tracer: Tracer) - defp fetch_token_balance( - %{ - token_contract_address_hash: token_contract_address_hash, - address_hash: address_hash, - block_number: block_number - } = token_balance - ) do - token_contract_address_hash - |> BalanceReader.get_balance_of(address_hash, block_number) - |> set_token_balance_value(token_balance) - end - defp set_token_balance_value({:ok, balance}, token_balance) do Map.merge(token_balance, %{value: balance, value_fetched_at: DateTime.utc_now(), error: nil}) end @@ -128,12 +87,6 @@ defmodule Indexer.TokenBalances do |> TokenBalance.Fetcher.async_fetch() end - defp format_task_results({:exit, :timeout}), do: {:error, :timeout} - defp format_task_results({:ok, token_balance}), do: token_balance - - defp ignore_killed_task({:error, :timeout}), do: false - defp ignore_killed_task(_token_balance), do: true - defp ignore_request_with_errors(%{value: nil, value_fetched_at: nil, error: _error}), do: false defp ignore_request_with_errors(_token_balance), do: true diff --git a/apps/indexer/test/indexer/token/fetcher_test.exs b/apps/indexer/test/indexer/token/fetcher_test.exs index d9841fdc91..85872a70c5 100644 --- a/apps/indexer/test/indexer/token/fetcher_test.exs +++ b/apps/indexer/test/indexer/token/fetcher_test.exs @@ -35,28 +35,35 @@ defmodule Indexer.Token.FetcherTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" - }, - %{ - id: "symbol", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) diff --git a/apps/indexer/test/indexer/token/metadata_updater_test.exs b/apps/indexer/test/indexer/token/metadata_updater_test.exs index e72097f2b4..c57d216aeb 100644 --- a/apps/indexer/test/indexer/token/metadata_updater_test.exs +++ b/apps/indexer/test/indexer/token/metadata_updater_test.exs @@ -17,32 +17,39 @@ defmodule Indexer.Token.MetadataUpdaterTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" - }, - %{ - id: "symbol", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) - pid = start_supervised!({MetadataUpdater, %{update_interval: 0}}) + pid = start_supervised!({MetadataUpdater, %{update_interval: 1}}) wait_for_results(fn -> updated = Repo.one!(from(t in Token, where: t.cataloged == true and not is_nil(t.name), limit: 1)) @@ -63,28 +70,35 @@ defmodule Indexer.Token.MetadataUpdaterTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> + fn requests, _opts -> {:ok, - [ - %{ - id: "decimals", - result: "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - %{ - id: "name", - result: - "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" - }, - %{ - id: "symbol", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" - }, - %{ - id: "totalSupply", - result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" - } - ]} + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} end ) diff --git a/apps/indexer/test/indexer/token_balance/fetcher_test.exs b/apps/indexer/test/indexer/token_balance/fetcher_test.exs index 2dac5b398c..9f5d392c15 100644 --- a/apps/indexer/test/indexer/token_balance/fetcher_test.exs +++ b/apps/indexer/test/indexer/token_balance/fetcher_test.exs @@ -45,11 +45,11 @@ defmodule Indexer.TokenBalance.FetcherTest do expect( EthereumJSONRPC.Mox, :json_rpc, - fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> {:ok, [ %{ - id: "balanceOf", + id: id, jsonrpc: "2.0", result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" } @@ -80,12 +80,12 @@ defmodule Indexer.TokenBalance.FetcherTest do EthereumJSONRPC.Mox, :json_rpc, 1, - fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> {:ok, [ %{ error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, - id: "balanceOf", + id: id, jsonrpc: "2.0" } ]} diff --git a/apps/indexer/test/indexer/token_balances_test.exs b/apps/indexer/test/indexer/token_balances_test.exs index 7809f67527..d9bc91080a 100644 --- a/apps/indexer/test/indexer/token_balances_test.exs +++ b/apps/indexer/test/indexer/token_balances_test.exs @@ -62,34 +62,6 @@ defmodule Indexer.TokenBalancesTest do assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []} end - - test "ignores results that raised :timeout" do - address = insert(:address) - token = insert(:token, contract_address: build(:contract_address)) - address_hash_string = Hash.to_string(address.hash) - - token_balance_params = [ - %{ - token_contract_address_hash: Hash.to_string(token.contract_address_hash), - address_hash: address_hash_string, - block_number: 1_000, - retries_count: 1 - }, - %{ - token_contract_address_hash: Hash.to_string(token.contract_address_hash), - address_hash: address_hash_string, - block_number: 1_001, - retries_count: 1 - } - ] - - get_balance_from_blockchain() - get_balance_from_blockchain_with_timeout(200) - - {:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(token_balance_params, timeout: 100) - - assert length(result) == 1 - end end describe "log_fetching_errors" do @@ -177,30 +149,11 @@ defmodule Indexer.TokenBalancesTest do expect( EthereumJSONRPC.Mox, :json_rpc, - fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: "balanceOf", - jsonrpc: "2.0", - result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" - } - ]} - end - ) - end - - defp get_balance_from_blockchain_with_timeout(timeout) do - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> - :timer.sleep(timeout) - + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> {:ok, [ %{ - id: "balanceOf", + id: id, jsonrpc: "2.0", result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" } @@ -213,12 +166,12 @@ defmodule Indexer.TokenBalancesTest do expect( EthereumJSONRPC.Mox, :json_rpc, - fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> {:ok, [ %{ error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, - id: "balanceOf", + id: id, jsonrpc: "2.0" } ]}