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`
pull/696/head
Sebastian Abondano 6 years ago
parent 44f4d8f591
commit a372acaa1d
  1. 40
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex
  2. 57
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  3. 3
      apps/block_scout_web/lib/block_scout_web/router.ex
  4. 13
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
  5. 76
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs
  6. 2
      apps/explorer/lib/explorer/chain.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

@ -234,6 +234,18 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil "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 %{ @status_type %{
type: "status", type: "status",
enum: ~s(["0", "1"]), 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 %{ @account_module %{
name: "account", name: "account",
actions: [ actions: [
@ -1158,12 +1207,18 @@ defmodule BlockScoutWeb.Etherscan do
actions: [@block_getblockreward_action] actions: [@block_getblockreward_action]
} }
@contract_module %{
name: "contract",
actions: [@contract_getabi_action]
}
@documentation [ @documentation [
@account_module, @account_module,
@logs_module, @logs_module,
@token_module, @token_module,
@stats_module, @stats_module,
@block_module @block_module,
@contract_module
] ]
def get_documentation do def get_documentation do

@ -29,7 +29,8 @@ defmodule BlockScoutWeb.Router do
"account" => RPC.AddressController, "account" => RPC.AddressController,
"logs" => RPC.LogsController, "logs" => RPC.LogsController,
"token" => RPC.TokenController, "token" => RPC.TokenController,
"stats" => RPC.StatsController "stats" => RPC.StatsController,
"contract" => RPC.ContractController
}) })
end end

@ -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

@ -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

@ -1429,7 +1429,7 @@ defmodule Explorer.Chain do
|> Repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name]) |> Repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name])
end 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 def address_hash_to_smart_contract(%Explorer.Chain.Hash{} = address_hash) do
query = query =
from( from(

Loading…
Cancel
Save