From a372acaa1d2972f244b9442ece6f8b170077e2c5 Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Mon, 10 Sep 2018 19:30:53 -0400 Subject: [PATCH] Add contract#getabi API endpoint Why: * For API users to be able to get the ABI for a given contract address. Example usage: ``` /api?module=contract&action=getabi&address={addressHash} ``` * Issue link: https://github.com/poanetwork/blockscout/issues/138 This change addresses the need by: * Editing router to support `contract#getabi` API endpoint. * Creating `API.RPC.ContractController.getabi/2` action to process requests to `contract#getabi` endpoint. * Creating `API.RPC.ContractView` to render `contract#getabi` responses. * Adding documentation data for the new `contract#getabi` API endpoint. Documentation data lives in `BlockScoutWeb.Etherscan` --- .../api/rpc/contract_controller.ex | 40 ++++++++++ .../lib/block_scout_web/etherscan.ex | 57 +++++++++++++- .../lib/block_scout_web/router.ex | 3 +- .../views/api/rpc/contract_view.ex | 13 ++++ .../api/rpc/contract_controller_test.exs | 76 +++++++++++++++++++ apps/explorer/lib/explorer/chain.ex | 2 +- 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex new file mode 100644 index 0000000000..f2fb31e2b8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -0,0 +1,40 @@ +defmodule BlockScoutWeb.API.RPC.ContractController do + use BlockScoutWeb, :controller + + alias Explorer.Chain + + def getabi(conn, params) do + with {:address_param, {:ok, address_param}} <- fetch_address(params), + {:format, {:ok, address_hash}} <- to_address_hash(address_param), + {:contract, {:ok, contract}} <- to_smart_contract(address_hash) do + render(conn, :getabi, %{abi: contract.abi}) + else + {:address_param, :error} -> + render(conn, :error, error: "Query parameter address is required") + + {:format, :error} -> + render(conn, :error, error: "Invalid address hash") + + {:contract, :not_found} -> + render(conn, :error, error: "Contract source code not verified") + end + end + + defp fetch_address(params) do + {:address_param, Map.fetch(params, "address")} + end + + defp to_address_hash(address_hash_string) do + {:format, Chain.string_to_address_hash(address_hash_string)} + end + + defp to_smart_contract(address_hash) do + result = + case Chain.address_hash_to_smart_contract(address_hash) do + nil -> :not_found + contract -> {:ok, contract} + end + + {:contract, result} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index a73e7866b1..c22a3a5dab 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -234,6 +234,18 @@ defmodule BlockScoutWeb.Etherscan do "result" => nil } + @contract_getabi_example_value %{ + "status" => "1", + "message" => "OK", + "result" => + ~s([{"constant":false,"inputs":[{"name":"voucher_token","type":"bytes32"}],"name":"burn","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"voucher_token","type":"bytes32"}],"name":"is_expired","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"voucher_token","type":"bytes32"}],"name":"is_burnt","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"voucher_token","type":"bytes32"},{"name":"_lifetime","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]) + } + + @contract_getabi_example_value_error %{ + "status" => "0", + "message" => "Contract source code not verified", + "result" => nil + } @status_type %{ type: "status", enum: ~s(["0", "1"]), @@ -1125,6 +1137,43 @@ defmodule BlockScoutWeb.Etherscan do ] } + @contract_getabi_action %{ + name: "getabi", + description: "Get ABI for verified contract.", + required_params: [ + %{ + key: "address", + placeholder: "addressHash", + type: "string", + description: "A 160-bit code used for identifying contracts." + } + ], + optional_params: [], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@contract_getabi_example_value), + model: %{ + name: "Result", + fields: %{ + status: @status_type, + message: @message_type, + result: %{ + type: "abi", + definition: "JSON string for the Application Binary Interface (ABI)" + } + } + } + }, + %{ + code: "200", + description: "error", + example_value: Jason.encode!(@contract_getabi_example_value_error) + } + ] + } + @account_module %{ name: "account", actions: [ @@ -1158,12 +1207,18 @@ defmodule BlockScoutWeb.Etherscan do actions: [@block_getblockreward_action] } + @contract_module %{ + name: "contract", + actions: [@contract_getabi_action] + } + @documentation [ @account_module, @logs_module, @token_module, @stats_module, - @block_module + @block_module, + @contract_module ] def get_documentation do diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 3f0d3b21f0..e6bfe0e5ba 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -29,7 +29,8 @@ defmodule BlockScoutWeb.Router do "account" => RPC.AddressController, "logs" => RPC.LogsController, "token" => RPC.TokenController, - "stats" => RPC.StatsController + "stats" => RPC.StatsController, + "contract" => RPC.ContractController }) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex new file mode 100644 index 0000000000..ff00cd7fea --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex @@ -0,0 +1,13 @@ +defmodule BlockScoutWeb.API.RPC.ContractView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.RPC.RPCView + + def render("getabi.json", %{abi: abi}) do + RPCView.render("show.json", data: Jason.encode!(abi)) + end + + def render("error.json", assigns) do + RPCView.render("error.json", assigns) + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs new file mode 100644 index 0000000000..747f965d3f --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -0,0 +1,76 @@ +defmodule BlockScoutWeb.API.RPC.ContractControllerTest do + use BlockScoutWeb.ConnCase + + describe "getabi" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getabi" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getabi", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address hash" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getabi", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == nil + assert response["status"] == "0" + assert response["message"] == "Contract source code not verified" + end + + test "with a verified contract address", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{ + "module" => "contract", + "action" => "getabi", + "address" => to_string(contract.address_hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == Jason.encode!(contract.abi) + assert response["status"] == "1" + assert response["message"] == "OK" + end + end +end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index b88e3409db..d2c7ec0aaa 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1429,7 +1429,7 @@ defmodule Explorer.Chain do |> Repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name]) end - @spec address_hash_to_smart_contract(%Explorer.Chain.Hash{}) :: %Explorer.Chain.SmartContract{} + @spec address_hash_to_smart_contract(%Explorer.Chain.Hash{}) :: %Explorer.Chain.SmartContract{} | nil def address_hash_to_smart_contract(%Explorer.Chain.Hash{} = address_hash) do query = from(