Merge branch 'ag-docs1' of https://github.com/andogro/poa-explorer into ag-docs1
@ -1,29 +0,0 @@ |
|||||||
$primary: #15bba6; |
|
||||||
$secondary: #17314f; |
|
||||||
$tertiary: #00ff00; |
|
||||||
|
|
||||||
$header-links-color-active: #333; |
|
||||||
$dashboard-banner-gradient-start: $secondary; |
|
||||||
$dashboard-banner-gradient-end: #1e4168; |
|
||||||
|
|
||||||
$dashboard-line-color-market: $primary; |
|
||||||
|
|
||||||
$tile-type-block-border-color: $secondary; |
|
||||||
$tile-type-block-color: #333; |
|
||||||
|
|
||||||
$footer-background-color: #173250; |
|
||||||
$footer-text-color: #909dac; |
|
||||||
|
|
||||||
$navbar-logo-height: auto; |
|
||||||
$navbar-logo-width: 100px; |
|
||||||
|
|
||||||
$footer-logo-height: auto; |
|
||||||
$footer-logo-width: 100px; |
|
||||||
|
|
||||||
$card-background-1: $secondary; |
|
||||||
$card-background-1-text-color: #fff; |
|
||||||
|
|
||||||
$btn-copy-color: $secondary; |
|
||||||
$btn-qr-color: $secondary; |
|
||||||
|
|
||||||
$btn-dropdown-line-color: $secondary; |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 4.9 KiB |
@ -0,0 +1,118 @@ |
|||||||
|
defmodule BlockScoutWeb.API.RPC.EthController do |
||||||
|
use BlockScoutWeb, :controller |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.Chain.Wei |
||||||
|
|
||||||
|
def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do |
||||||
|
responses = responses(requests) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render("responses.json", %{responses: responses}) |
||||||
|
end |
||||||
|
|
||||||
|
def eth_request(%{body_params: %{"_json" => request}} = conn, _) do |
||||||
|
[response] = responses([request]) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render("response.json", %{response: response}) |
||||||
|
end |
||||||
|
|
||||||
|
def eth_request(conn, request) do |
||||||
|
# In the case that the JSON body is sent up w/o a json content type, |
||||||
|
# Phoenix encodes it as a single key value pair, with the value being |
||||||
|
# nil and the body being the key (as in a CURL request w/ no content type header) |
||||||
|
decoded_request = |
||||||
|
with [{single_key, nil}] <- Map.to_list(request), |
||||||
|
{:ok, decoded} <- Jason.decode(single_key) do |
||||||
|
decoded |
||||||
|
else |
||||||
|
_ -> request |
||||||
|
end |
||||||
|
|
||||||
|
[response] = responses([decoded_request]) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render("response.json", %{response: response}) |
||||||
|
end |
||||||
|
|
||||||
|
defp responses(requests) do |
||||||
|
Enum.map(requests, fn request -> |
||||||
|
with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")}, |
||||||
|
{:request, {:ok, result}} <- {:request, do_eth_request(request)} do |
||||||
|
format_success(result, id) |
||||||
|
else |
||||||
|
{:id, :error} -> format_error("id is a required field", 0) |
||||||
|
{:request, {:error, message}} -> format_error(message, Map.get(request, "id")) |
||||||
|
end |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp format_success(result, id) do |
||||||
|
%{result: result, id: id} |
||||||
|
end |
||||||
|
|
||||||
|
defp format_error(message, id) do |
||||||
|
%{error: message, id: id} |
||||||
|
end |
||||||
|
|
||||||
|
defp do_eth_request(%{"jsonrpc" => rpc_version}) when rpc_version != "2.0" do |
||||||
|
{:error, "invalid rpc version"} |
||||||
|
end |
||||||
|
|
||||||
|
defp do_eth_request(%{"jsonrpc" => "2.0", "method" => method, "params" => params}) |
||||||
|
when is_list(params) do |
||||||
|
with {:ok, action} <- get_action(method), |
||||||
|
true <- :erlang.function_exported(__MODULE__, action, Enum.count(params)) do |
||||||
|
apply(__MODULE__, action, params) |
||||||
|
else |
||||||
|
_ -> |
||||||
|
{:error, "Action not found."} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp do_eth_request(%{"params" => _params, "method" => _}) do |
||||||
|
{:error, "Invalid params. Params must be a list."} |
||||||
|
end |
||||||
|
|
||||||
|
defp do_eth_request(_) do |
||||||
|
{:error, "Method, params, and jsonrpc, are all required parameters."} |
||||||
|
end |
||||||
|
|
||||||
|
def eth_get_balance(address_param, block_param \\ nil) do |
||||||
|
with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)}, |
||||||
|
{:block, {:ok, block}} <- {:block, block_param(block_param)}, |
||||||
|
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address, block)} do |
||||||
|
{:ok, Wei.hex_format(balance)} |
||||||
|
else |
||||||
|
{:address, :error} -> |
||||||
|
{:error, "Query parameter 'address' is invalid"} |
||||||
|
|
||||||
|
{:block, :error} -> |
||||||
|
{:error, "Query parameter 'block' is invalid"} |
||||||
|
|
||||||
|
{:balance, {:error, :not_found}} -> |
||||||
|
{:error, "Balance not found"} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp get_action("eth_getBalance"), do: {:ok, :eth_get_balance} |
||||||
|
defp get_action(_), do: :error |
||||||
|
|
||||||
|
defp block_param("latest"), do: {:ok, :latest} |
||||||
|
defp block_param("earliest"), do: {:ok, :earliest} |
||||||
|
defp block_param("pending"), do: {:ok, :pending} |
||||||
|
|
||||||
|
defp block_param(string_integer) when is_bitstring(string_integer) do |
||||||
|
case Integer.parse(string_integer) do |
||||||
|
{integer, ""} -> {:ok, integer} |
||||||
|
_ -> :error |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp block_param(nil), do: {:ok, :latest} |
||||||
|
defp block_param(_), do: :error |
||||||
|
end |
@ -0,0 +1,8 @@ |
|||||||
|
defmodule BlockScoutWeb.PageNotFoundController do |
||||||
|
use BlockScoutWeb, :controller |
||||||
|
|
||||||
|
def index(conn, _params) do |
||||||
|
conn |
||||||
|
|> render("index.html") |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,13 @@ |
|||||||
|
defmodule BlockScoutWeb.API.RPC.EthView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.RPC.EthRPCView |
||||||
|
|
||||||
|
def render("responses.json", %{responses: responses}) do |
||||||
|
EthRPCView.render("responses.json", %{responses: responses}) |
||||||
|
end |
||||||
|
|
||||||
|
def render("response.json", %{response: response}) do |
||||||
|
EthRPCView.render("response.json", %{response: response}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
defmodule BlockScoutWeb.PageNotFoundView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
|
||||||
|
@dialyzer :no_match |
||||||
|
end |
@ -0,0 +1,199 @@ |
|||||||
|
defmodule BlockScoutWeb.API.RPC.EthControllerTest do |
||||||
|
use BlockScoutWeb.ConnCase, async: false |
||||||
|
|
||||||
|
alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime} |
||||||
|
alias Indexer.Fetcher.CoinBalanceOnDemand |
||||||
|
|
||||||
|
setup do |
||||||
|
mocked_json_rpc_named_arguments = [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [] |
||||||
|
] |
||||||
|
|
||||||
|
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) |
||||||
|
start_supervised!(AverageBlockTime) |
||||||
|
start_supervised!({CoinBalanceOnDemand, [mocked_json_rpc_named_arguments, [name: CoinBalanceOnDemand]]}) |
||||||
|
start_supervised!(AddressesWithBalanceCounter) |
||||||
|
|
||||||
|
Application.put_env(:explorer, AverageBlockTime, enabled: true) |
||||||
|
|
||||||
|
on_exit(fn -> |
||||||
|
Application.put_env(:explorer, AverageBlockTime, enabled: false) |
||||||
|
end) |
||||||
|
|
||||||
|
:ok |
||||||
|
end |
||||||
|
|
||||||
|
defp params(api_params, params), do: Map.put(api_params, "params", params) |
||||||
|
|
||||||
|
describe "eth_get_balance" do |
||||||
|
setup do |
||||||
|
%{ |
||||||
|
api_params: %{ |
||||||
|
"method" => "eth_getBalance", |
||||||
|
"jsonrpc" => "2.0", |
||||||
|
"id" => 0 |
||||||
|
} |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
test "with an invalid address", %{conn: conn, api_params: api_params} do |
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, ["badHash"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert %{"error" => "Query parameter 'address' is invalid"} = response |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address that has no balance", %{conn: conn, api_params: api_params} do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash)])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert %{"error" => "Balance not found"} = response |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address that has a balance", %{conn: conn, api_params: api_params} do |
||||||
|
block = insert(:block) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash)])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert %{"result" => "0x1"} = response |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address that has no earliest balance", %{conn: conn, api_params: api_params} do |
||||||
|
block = insert(:block, number: 1) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["error"] == "Balance not found" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address that has an earliest balance", %{conn: conn, api_params: api_params} do |
||||||
|
block = insert(:block, number: 0) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["result"] == "0x1" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address and no pending balance", %{conn: conn, api_params: api_params} do |
||||||
|
block = insert(:block, number: 1, consensus: true) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["error"] == "Balance not found" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address and a pending balance", %{conn: conn, api_params: api_params} do |
||||||
|
block = insert(:block, number: 1, consensus: false) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["result"] == "0x1" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a valid address and a pending balance after a consensus block", %{conn: conn, api_params: api_params} do |
||||||
|
insert(:block, number: 1, consensus: true) |
||||||
|
block = insert(:block, number: 2, consensus: false) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["result"] == "0x1" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a block provided", %{conn: conn, api_params: api_params} do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1) |
||||||
|
insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2) |
||||||
|
insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["result"] == "0x2" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a block provided and no balance", %{conn: conn, api_params: api_params} do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"])) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert response["error"] == "Balance not found" |
||||||
|
end |
||||||
|
|
||||||
|
test "with a batch of requests", %{conn: conn} do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1) |
||||||
|
insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2) |
||||||
|
insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) |
||||||
|
|
||||||
|
params = [ |
||||||
|
%{"id" => 0, "params" => [to_string(address.hash), "1"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}, |
||||||
|
%{"id" => 1, "params" => [to_string(address.hash), "2"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}, |
||||||
|
%{"id" => 2, "params" => [to_string(address.hash), "3"], "jsonrpc" => "2.0", "method" => "eth_getBalance"} |
||||||
|
] |
||||||
|
|
||||||
|
assert response = |
||||||
|
conn |
||||||
|
|> put_req_header("content-type", "application/json") |
||||||
|
|> post("/api/eth_rpc", Jason.encode!(params)) |
||||||
|
|> json_response(200) |
||||||
|
|
||||||
|
assert [ |
||||||
|
%{"id" => 0, "result" => "0x1"}, |
||||||
|
%{"id" => 1, "result" => "0x2"}, |
||||||
|
%{"id" => 2, "result" => "0x3"} |
||||||
|
] = response |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,44 @@ |
|||||||
|
defmodule Explorer.Chain.NetVersionCache do |
||||||
|
@moduledoc """ |
||||||
|
Caches chain version. |
||||||
|
""" |
||||||
|
|
||||||
|
@cache_name :net_version |
||||||
|
@key :version |
||||||
|
|
||||||
|
@spec version() :: non_neg_integer() | {:error, any()} |
||||||
|
def version do |
||||||
|
cached_value = fetch_from_cache() |
||||||
|
|
||||||
|
if is_nil(cached_value) do |
||||||
|
fetch_from_node() |
||||||
|
else |
||||||
|
cached_value |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def cache_name do |
||||||
|
@cache_name |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_from_cache do |
||||||
|
ConCache.get(@cache_name, @key) |
||||||
|
end |
||||||
|
|
||||||
|
defp cache_value(value) do |
||||||
|
ConCache.put(@cache_name, @key, value) |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_from_node do |
||||||
|
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) |
||||||
|
|
||||||
|
case EthereumJSONRPC.fetch_net_version(json_rpc_named_arguments) do |
||||||
|
{:ok, value} -> |
||||||
|
cache_value(value) |
||||||
|
value |
||||||
|
|
||||||
|
other -> |
||||||
|
other |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
defmodule Explorer.Repo.Migrations.AddTxHashInsertedAtIndex do |
||||||
|
use Ecto.Migration |
||||||
|
|
||||||
|
def change do |
||||||
|
create(index(:transactions, [:hash, :inserted_at])) |
||||||
|
end |
||||||
|
end |