diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 1a5ffdeac2..788d8d4242 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -8,9 +8,10 @@ defmodule Explorer.SmartContract.Reader do alias Explorer.Chain alias EthereumJSONRPC.Encoder + alias Explorer.Chain.Hash @doc """ - Queries a contract function on the blockchain and returns the call result. + Queries the contract functions on the blockchain and returns the call results. ## Examples @@ -23,9 +24,9 @@ defmodule Explorer.SmartContract.Reader do ) # => %{"sum" => [42]} """ - @spec query_contract(String.t(), %{String.t() => [term()]}) :: map() - def query_contract(contract_address, functions) do - {:ok, address_hash} = Chain.string_to_address_hash(contract_address) + @spec query_contract(%Explorer.Chain.Hash{}, %{String.t() => [term()]}) :: map() + def query_contract(address_hash, functions) do + contract_address = Hash.to_string(address_hash) abi = address_hash @@ -52,4 +53,139 @@ defmodule Explorer.SmartContract.Reader do id: function_name } end + + @doc """ + List all the smart contract functions with its current value from the + blockchain, following the ABI order. + + Functions that require arguments can be queryable but won't list the current + value at this moment. + + ## Examples + + $ Explorer.SmartContract.Reader.read_only_functions("0x798465571ae21a184a272f044f991ad1d5f87a3f") + => [ + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256", "value" => 0}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "with_arguments", + "outputs" => [%{"name" => "", "type" => "bool", "value" => ""}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + """ + @spec read_only_functions(%Explorer.Chain.Hash{}) :: [%{}] + def read_only_functions(contract_address_hash) do + contract_address_hash + |> Chain.address_hash_to_smart_contract() + |> Map.get(:abi, []) + |> Enum.filter(& &1["constant"]) + |> fetch_current_value_from_blockchain(contract_address_hash, []) + |> Enum.reverse() + end + + def fetch_current_value_from_blockchain([%{"inputs" => []} = function | tail], contract_address_hash, acc) do + values = + fetch_from_blockchain(contract_address_hash, %{ + name: function["name"], + args: function["inputs"], + outputs: function["outputs"] + }) + + formatted = Map.replace!(function, "outputs", values) + + fetch_current_value_from_blockchain(tail, contract_address_hash, [formatted | acc]) + end + + def fetch_current_value_from_blockchain([function | tail], contract_address_hash, acc) do + values = link_outputs_and_values(%{}, Map.get(function, "outputs", []), function["name"]) + + formatted = Map.replace!(function, "outputs", values) + + fetch_current_value_from_blockchain(tail, contract_address_hash, [formatted | acc]) + end + + def fetch_current_value_from_blockchain([], _contract_address_hash, acc), do: acc + + @doc """ + Fetches the blockchain value of a function that requires arguments. + """ + @spec query_function(String.t(), %{name: String.t(), args: nil}) :: [%{}] + def query_function(contract_address_hash, %{name: name, args: nil}) do + query_function(contract_address_hash, %{name: name, args: []}) + end + + @spec query_function(%Explorer.Chain.Hash{}, %{name: String.t(), args: [term()]}) :: [%{}] + def query_function(contract_address_hash, %{name: name, args: args}) do + function = + contract_address_hash + |> Chain.address_hash_to_smart_contract() + |> Map.get(:abi, []) + |> Enum.filter(fn function -> function["name"] == name end) + |> List.first() + + fetch_from_blockchain(contract_address_hash, %{name: name, args: args, outputs: function["outputs"]}) + end + + defp fetch_from_blockchain(contract_address_hash, %{name: name, args: args, outputs: outputs}) do + contract_address_hash + |> query_contract(%{name => args}) + |> link_outputs_and_values(outputs, name) + end + + @doc """ + The type of the arguments passed to the blockchain interferes in the output, + but we always get strings from the front, so it is necessary to normalize it. + """ + def normalize_args(args) do + Enum.map(args, &parse_item/1) + end + + defp parse_item("true"), do: true + defp parse_item("false"), do: false + + defp parse_item(item) do + response = Integer.parse(item) + + case response do + {integer, remainder_of_binary} when remainder_of_binary == "" -> integer + _ -> item + end + end + + def link_outputs_and_values(blockchain_values, outputs, function_name) do + values = Map.get(blockchain_values, function_name, [""]) + + for output <- outputs, value <- values do + new_value(output, value) + end + end + + defp new_value(%{"type" => "address"} = output, value) do + Map.put_new(output, "value", bytes_to_string(value)) + end + + defp new_value(%{"type" => "bytes" <> _number} = output, value) do + Map.put_new(output, "value", bytes_to_string(value)) + end + + defp new_value(output, value) do + Map.put_new(output, "value", value) + end + + @spec bytes_to_string(<<_::_*8>>) :: String.t() + defp bytes_to_string(value) do + Hash.to_string(%Hash{byte_count: byte_size(value), bytes: value}) + end end diff --git a/apps/explorer/test/explorer/smart_contract/reader_test.exs b/apps/explorer/test/explorer/smart_contract/reader_test.exs index c9fc37ecba..d082a63094 100644 --- a/apps/explorer/test/explorer/smart_contract/reader_test.exs +++ b/apps/explorer/test/explorer/smart_contract/reader_test.exs @@ -5,35 +5,37 @@ defmodule Explorer.SmartContract.ReaderTest do doctest Explorer.SmartContract.Reader alias Explorer.SmartContract.Reader + alias Plug.Conn - alias Explorer.Chain.Hash @ethereum_jsonrpc_original Application.get_env(:ethereum_jsonrpc, :url) - describe "query_contract/2" do - setup do - bypass = Bypass.open() + setup do + bypass = Bypass.open() - Application.put_env(:ethereum_jsonrpc, :url, "http://localhost:#{bypass.port}") + Application.put_env(:ethereum_jsonrpc, :url, "http://localhost:#{bypass.port}") - on_exit(fn -> - Application.put_env(:ethereum_jsonrpc, :url, @ethereum_jsonrpc_original) - end) - - {:ok, bypass: bypass} - end + on_exit(fn -> + Application.put_env(:ethereum_jsonrpc, :url, @ethereum_jsonrpc_original) + end) - test "correctly returns the result of a smart contract function", %{bypass: bypass} do - blockchain_resp = - "[{\"jsonrpc\":\"2.0\",\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"id\":\"get\"}]\n" + {:ok, bypass: bypass} + end - Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, blockchain_resp) end) + describe "query_contract/2" do + test "correctly returns the results of the smart contract functions", %{bypass: bypass} do + Bypass.expect(bypass, fn conn -> + Conn.resp( + conn, + 200, + ~s[{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":"get"}] + ) + end) hash = :smart_contract |> insert() |> Map.get(:address_hash) - |> Hash.to_string() assert Reader.query_contract(hash, %{"get" => []}) == %{"get" => [0]} end @@ -51,4 +53,139 @@ defmodule Explorer.SmartContract.ReaderTest do ) == %{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", %{bypass: bypass} do + smart_contract = + insert( + :smart_contract, + abi: [ + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "with_arguments", + "outputs" => [%{"name" => "", "type" => "bool"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + ) + + Bypass.expect(bypass, fn conn -> + Conn.resp( + conn, + 200, + ~s[{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":"get"}] + ) + end) + + response = Reader.read_only_functions(smart_contract.address_hash) + + assert [ + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256", "value" => 0}], + "payable" => _, + "stateMutability" => _, + "type" => _ + }, + %{ + "constant" => true, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "with_arguments", + "outputs" => [%{"name" => "", "type" => "bool", "value" => ""}], + "payable" => _, + "stateMutability" => _, + "type" => _ + } + ] = response + end + end + + describe "query_function/2" do + test "given the arguments, fetches the function value from the blockchain", %{bypass: bypass} do + smart_contract = insert(:smart_contract) + + Bypass.expect(bypass, fn conn -> + Conn.resp( + conn, + 200, + ~s[{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":"get"}] + ) + end) + + assert [ + %{ + "name" => "", + "type" => "uint256", + "value" => 0 + } + ] = Reader.query_function(smart_contract.address_hash, %{name: "get", args: []}) + end + end + + describe "normalize_args/1" do + test "converts argument when is a number" do + assert [0] = Reader.normalize_args(["0"]) + + assert ["0x798465571ae21a184a272f044f991ad1d5f87a3f"] = + Reader.normalize_args(["0x798465571ae21a184a272f044f991ad1d5f87a3f"]) + end + + test "converts argument when is a boolean" do + assert [true] = Reader.normalize_args(["true"]) + assert [false] = Reader.normalize_args(["false"]) + + assert ["some string"] = Reader.normalize_args(["some string"]) + end + end + + describe "link_outputs_and_values/2" do + test "links the ABI outputs with the values retrieved from the blockchain" do + blockchain_values = %{ + "getOwner" => [ + <<105, 55, 203, 37, 235, 84, 188, 1, 59, 156, 19, 196, 122, 179, 142, 182, 62, 221, 20, 147>> + ] + } + + outputs = [%{"name" => "", "type" => "address"}] + + function_name = "getOwner" + + assert [%{"name" => "", "type" => "address", "value" => "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"}] = + Reader.link_outputs_and_values(blockchain_values, outputs, function_name) + end + + test "correctly shows returns of 'bytes' type" do + blockchain_values = %{ + "get" => [ + <<0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> + ] + } + + outputs = [%{"name" => "", "type" => "bytes32"}] + + function_name = "get" + + assert [ + %{ + "name" => "", + "type" => "bytes32", + "value" => "0x000a000000000000000000000000000000000000000000000000000000000000" + } + ] = Reader.link_outputs_and_values(blockchain_values, outputs, function_name) + end + end end