commit
843eb4efce
@ -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,89 @@ |
||||
defmodule Explorer.Chain.BlocksCache do |
||||
@moduledoc """ |
||||
Caches the last imported blocks |
||||
""" |
||||
|
||||
alias Explorer.Repo |
||||
|
||||
@block_numbers_key "block_numbers" |
||||
@cache_name :blocks |
||||
@number_of_elements 60 |
||||
|
||||
def update(block) do |
||||
numbers = block_numbers() |
||||
|
||||
max_number = if numbers == [], do: -1, else: Enum.max(numbers) |
||||
min_number = if numbers == [], do: -1, else: Enum.min(numbers) |
||||
|
||||
in_range? = block.number > min_number && Enum.all?(numbers, fn number -> number != block.number end) |
||||
not_too_far_away? = block.number > max_number - @number_of_elements - 1 |
||||
|
||||
if (block.number > max_number || Enum.count(numbers) == 1 || in_range?) && not_too_far_away? do |
||||
if Enum.count(numbers) >= @number_of_elements do |
||||
remove_block(numbers) |
||||
put_block(block, List.delete(numbers, Enum.min(numbers))) |
||||
else |
||||
put_block(block, numbers) |
||||
end |
||||
end |
||||
end |
||||
|
||||
def rewrite_cache(elements) do |
||||
numbers = block_numbers() |
||||
|
||||
ConCache.delete(@cache_name, @block_numbers_key) |
||||
|
||||
numbers |
||||
|> Enum.each(fn number -> |
||||
ConCache.delete(@cache_name, number) |
||||
end) |
||||
|
||||
elements |
||||
|> Enum.reduce([], fn element, acc -> |
||||
put_block(element, acc) |
||||
|
||||
[element.number | acc] |
||||
end) |
||||
end |
||||
|
||||
def enough_elements?(number) do |
||||
ConCache.size(@cache_name) > number |
||||
end |
||||
|
||||
def update_blocks(blocks) do |
||||
Enum.each(blocks, fn block -> |
||||
update(block) |
||||
end) |
||||
end |
||||
|
||||
def blocks(number \\ nil) do |
||||
numbers = block_numbers() |
||||
|
||||
number = if is_nil(number), do: Enum.count(numbers), else: number |
||||
|
||||
numbers |
||||
|> Enum.sort() |
||||
|> Enum.reverse() |
||||
|> Enum.slice(0, number) |
||||
|> Enum.map(fn number -> |
||||
ConCache.get(@cache_name, number) |
||||
end) |
||||
end |
||||
|
||||
def cache_name, do: @cache_name |
||||
|
||||
def block_numbers do |
||||
ConCache.get(@cache_name, @block_numbers_key) || [] |
||||
end |
||||
|
||||
defp remove_block(numbers) do |
||||
min_number = Enum.min(numbers) |
||||
ConCache.delete(@cache_name, min_number) |
||||
end |
||||
|
||||
defp put_block(block, numbers) do |
||||
block_with_preloads = Repo.preload(block, [:transactions, [miner: :names], :rewards]) |
||||
ConCache.put(@cache_name, block.number, block_with_preloads) |
||||
ConCache.put(@cache_name, @block_numbers_key, [block.number | numbers]) |
||||
end |
||||
end |
@ -0,0 +1,84 @@ |
||||
defmodule Explorer.Chain.BlocksCacheTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Chain.BlocksCache |
||||
alias Explorer.Repo |
||||
|
||||
setup do |
||||
Supervisor.terminate_child(Explorer.Supervisor, ConCache) |
||||
Supervisor.restart_child(Explorer.Supervisor, ConCache) |
||||
:ok |
||||
end |
||||
|
||||
describe "update/1" do |
||||
test "adds a new value to cache" do |
||||
block = insert(:block) |> Repo.preload([:transactions, [miner: :names], :rewards]) |
||||
|
||||
BlocksCache.update(block) |
||||
|
||||
assert BlocksCache.blocks() == [block] |
||||
end |
||||
|
||||
test "adds a new elements removing the oldest one" do |
||||
blocks = |
||||
1..60 |
||||
|> Enum.map(fn number -> |
||||
block = insert(:block, number: number) |
||||
|
||||
BlocksCache.update(block) |
||||
|
||||
block.number |
||||
end) |
||||
|
||||
new_block = insert(:block, number: 70) |
||||
BlocksCache.update(new_block) |
||||
|
||||
new_blocks = blocks |> List.replace_at(0, new_block.number) |> Enum.sort() |> Enum.reverse() |
||||
|
||||
assert Enum.map(BlocksCache.blocks(), & &1.number) == new_blocks |
||||
end |
||||
|
||||
test "does not add too old blocks" do |
||||
block = insert(:block, number: 100_000) |> Repo.preload([:transactions, [miner: :names], :rewards]) |
||||
old_block = insert(:block, number: 1_000) |
||||
|
||||
BlocksCache.update(block) |
||||
BlocksCache.update(old_block) |
||||
|
||||
assert BlocksCache.blocks() == [block] |
||||
end |
||||
|
||||
test "adds missing element" do |
||||
block1 = insert(:block, number: 10) |
||||
block2 = insert(:block, number: 4) |
||||
|
||||
BlocksCache.update(block1) |
||||
BlocksCache.update(block2) |
||||
|
||||
assert Enum.count(BlocksCache.blocks()) == 2 |
||||
|
||||
block3 = insert(:block, number: 6) |
||||
|
||||
BlocksCache.update(block3) |
||||
|
||||
assert Enum.map(BlocksCache.blocks(), & &1.number) == [10, 6, 4] |
||||
end |
||||
end |
||||
|
||||
describe "rewrite_cache/1" do |
||||
test "updates cache" do |
||||
block = insert(:block) |
||||
|
||||
BlocksCache.update(block) |
||||
|
||||
block1 = insert(:block) |> Repo.preload([:transactions, [miner: :names], :rewards]) |
||||
block2 = insert(:block) |> Repo.preload([:transactions, [miner: :names], :rewards]) |
||||
|
||||
new_blocks = [block1, block2] |
||||
|
||||
BlocksCache.rewrite_cache(new_blocks) |
||||
|
||||
assert BlocksCache.blocks() == [block2, block1] |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue