* master: (180 commits) make block difficulty params optional feat: document eth rpc api mimicking endpoints feat: add eth_getLogs rpc endpoint Update CHANGELOG.md eth classic and sokol logos fix sokol and eth classic logo fix fix parity test Update CHANGELOG.md Update _footer.html.eex fix tests fix config increase request idle timeout Added additional chains, wobserver info fix test fix tests add CHANGELOG entry mix format use milliseconds in cache fix tests fix test ... # Conflicts: # CHANGELOG.md # apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex # apps/block_scout_web/priv/gettext/default.pot # apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.popull/2263/head
@ -1,13 +1,17 @@ |
||||
$btn-dropdown-line-bg: #fff !default; |
||||
$btn-dropdown-line-color: $primary !default; |
||||
$btn-dropdown-line-color: #e2e5ec !default; |
||||
$btn-dropdown-line-color-hover: #f5f6fa !default; |
||||
$btn-dropdown-line-font: #333; |
||||
|
||||
.btn-dropdown-line { |
||||
@include btn-line($btn-dropdown-line-bg, $btn-dropdown-line-color); |
||||
outline: none !important; |
||||
color: #333; |
||||
border-color: #e2e5ec; |
||||
&:hover { |
||||
background-color: transparent; |
||||
color: #333; |
||||
} |
||||
} |
||||
@include btn-line($btn-dropdown-line-bg, $btn-dropdown-line-color); |
||||
border-color: $btn-dropdown-line-color; |
||||
color: $btn-dropdown-line-font; |
||||
outline: none !important; |
||||
|
||||
&:hover { |
||||
background-color: $btn-dropdown-line-color-hover; |
||||
border-color: $btn-dropdown-line-color; |
||||
color: $btn-dropdown-line-font; |
||||
} |
||||
} |
||||
|
@ -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: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 40 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 |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -0,0 +1,355 @@ |
||||
defmodule BlockScoutWeb.API.RPC.EthController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Ecto.Type, as: EctoType |
||||
alias Explorer.{Chain, Repo} |
||||
alias Explorer.Chain.{Block, Data, Hash, Hash.Address, Wei} |
||||
alias Explorer.Etherscan.Logs |
||||
|
||||
@methods %{ |
||||
"eth_getBalance" => %{ |
||||
action: :eth_get_balance, |
||||
notes: """ |
||||
the `earliest` parameter will not work as expected currently, because genesis block balances |
||||
are not currently imported |
||||
""" |
||||
}, |
||||
"eth_getLogs" => %{ |
||||
action: :eth_get_logs, |
||||
notes: """ |
||||
Will never return more than 1000 log entries. |
||||
""" |
||||
} |
||||
} |
||||
|
||||
@index_to_word %{ |
||||
0 => "first", |
||||
1 => "second", |
||||
2 => "third", |
||||
3 => "fourth" |
||||
} |
||||
|
||||
def methods, do: @methods |
||||
|
||||
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 |
||||
|
||||
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 |
||||
|
||||
def eth_get_logs(filter_options) do |
||||
with {:ok, address_or_topic_params} <- address_or_topic_params(filter_options), |
||||
{:ok, from_block_param, to_block_param} <- logs_blocks_filter(filter_options), |
||||
{:ok, from_block} <- cast_block(from_block_param), |
||||
{:ok, to_block} <- cast_block(to_block_param) do |
||||
filter = |
||||
address_or_topic_params |
||||
|> Map.put(:from_block, from_block) |
||||
|> Map.put(:to_block, to_block) |
||||
|> Map.put(:allow_non_consensus, true) |
||||
|
||||
{:ok, filter |> Logs.list_logs() |> Enum.map(&render_log/1)} |
||||
else |
||||
{:error, message} when is_bitstring(message) -> |
||||
{:error, message} |
||||
|
||||
{:error, :empty} -> |
||||
{:ok, []} |
||||
|
||||
_ -> |
||||
{:error, "Something went wrong."} |
||||
end |
||||
end |
||||
|
||||
defp render_log(log) do |
||||
topics = |
||||
Enum.reject( |
||||
[log.first_topic, log.second_topic, log.third_topic, log.fourth_topic], |
||||
&is_nil/1 |
||||
) |
||||
|
||||
%{ |
||||
"address" => to_string(log.address_hash), |
||||
"blockHash" => to_string(log.block_hash), |
||||
"blockNumber" => Integer.to_string(log.block_number, 16), |
||||
"data" => to_string(log.data), |
||||
"logIndex" => Integer.to_string(log.index, 16), |
||||
"removed" => log.block_consensus == false, |
||||
"topics" => topics, |
||||
"transactionHash" => to_string(log.transaction_hash), |
||||
"transactionIndex" => log.transaction_index, |
||||
"transactionLogIndex" => log.index, |
||||
"type" => "mined" |
||||
} |
||||
end |
||||
|
||||
defp cast_block("0x" <> hexadecimal_digits = input) do |
||||
case Integer.parse(hexadecimal_digits, 16) do |
||||
{integer, ""} -> {:ok, integer} |
||||
_ -> {:error, input <> " is not a valid block number"} |
||||
end |
||||
end |
||||
|
||||
defp cast_block(integer) when is_integer(integer), do: {:ok, integer} |
||||
defp cast_block(_), do: {:error, "invalid block number"} |
||||
|
||||
defp address_or_topic_params(filter_options) do |
||||
address_param = Map.get(filter_options, "address") |
||||
topics_param = Map.get(filter_options, "topics") |
||||
|
||||
with {:ok, address} <- validate_address(address_param), |
||||
{:ok, topics} <- validate_topics(topics_param) do |
||||
address_and_topics(address, topics) |
||||
end |
||||
end |
||||
|
||||
defp address_and_topics(nil, nil), do: {:error, "Must supply one of address and topics"} |
||||
defp address_and_topics(address, nil), do: {:ok, %{address_hash: address}} |
||||
defp address_and_topics(nil, topics), do: {:ok, topics} |
||||
defp address_and_topics(address, topics), do: {:ok, Map.put(topics, :address_hash, address)} |
||||
|
||||
defp validate_address(nil), do: {:ok, nil} |
||||
|
||||
defp validate_address(address) do |
||||
case Address.cast(address) do |
||||
{:ok, address} -> {:ok, address} |
||||
:error -> {:error, "invalid address"} |
||||
end |
||||
end |
||||
|
||||
defp validate_topics(nil), do: {:ok, nil} |
||||
defp validate_topics([]), do: [] |
||||
|
||||
defp validate_topics(topics) when is_list(topics) do |
||||
topics |
||||
|> Stream.with_index() |
||||
|> Enum.reduce({:ok, %{}}, fn {topic, index}, {:ok, acc} -> |
||||
case cast_topics(topic) do |
||||
{:ok, data} -> |
||||
with_filter = Map.put(acc, String.to_existing_atom("#{@index_to_word[index]}_topic"), data) |
||||
|
||||
{:ok, add_operator(with_filter, index)} |
||||
|
||||
:error -> |
||||
{:error, "invalid topics"} |
||||
end |
||||
end) |
||||
end |
||||
|
||||
defp add_operator(filters, 0), do: filters |
||||
|
||||
defp add_operator(filters, index) do |
||||
Map.put(filters, String.to_existing_atom("topic#{index - 1}_#{index}_opr"), "and") |
||||
end |
||||
|
||||
defp cast_topics(topics) when is_list(topics) do |
||||
case EctoType.cast({:array, Data}, topics) do |
||||
{:ok, data} -> {:ok, Enum.map(data, &to_string/1)} |
||||
:error -> :error |
||||
end |
||||
end |
||||
|
||||
defp cast_topics(topic) do |
||||
case Data.cast(topic) do |
||||
{:ok, data} -> {:ok, to_string(data)} |
||||
:error -> :error |
||||
end |
||||
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 logs_blocks_filter(filter_options) do |
||||
with {:filter, %{"blockHash" => block_hash_param}} <- {:filter, filter_options}, |
||||
{:block_hash, {:ok, block_hash}} <- {:block_hash, Hash.Full.cast(block_hash_param)}, |
||||
{:block, %{number: number}} <- {:block, Repo.get(Block, block_hash)} do |
||||
{:ok, number, number} |
||||
else |
||||
{:filter, filters} -> |
||||
from_block = Map.get(filters, "fromBlock", "latest") |
||||
to_block = Map.get(filters, "toBlock", "latest") |
||||
|
||||
max_block_number = |
||||
if from_block == "latest" || to_block == "latest" do |
||||
max_consensus_block_number() |
||||
end |
||||
|
||||
pending_block_number = |
||||
if from_block == "pending" || to_block == "pending" do |
||||
max_non_consensus_block_number(max_block_number) |
||||
end |
||||
|
||||
if is_nil(pending_block_number) && from_block == "pending" && to_block == "pending" do |
||||
{:error, :empty} |
||||
else |
||||
to_block_numbers(from_block, to_block, max_block_number, pending_block_number) |
||||
end |
||||
|
||||
{:block, _} -> |
||||
{:error, "Invalid Block Hash"} |
||||
|
||||
{:block_hash, _} -> |
||||
{:error, "Invalid Block Hash"} |
||||
end |
||||
end |
||||
|
||||
defp to_block_numbers(from_block, to_block, max_block_number, pending_block_number) do |
||||
actual_pending_block_number = pending_block_number || max_block_number |
||||
|
||||
with {:ok, from} <- |
||||
to_block_number(from_block, max_block_number, actual_pending_block_number), |
||||
{:ok, to} <- to_block_number(to_block, max_block_number, actual_pending_block_number) do |
||||
{:ok, from, to} |
||||
end |
||||
end |
||||
|
||||
defp to_block_number(integer, _, _) when is_integer(integer), do: {:ok, integer} |
||||
defp to_block_number("latest", max_block_number, _), do: {:ok, max_block_number || 0} |
||||
defp to_block_number("earliest", _, _), do: {:ok, 0} |
||||
defp to_block_number("pending", max_block_number, nil), do: {:ok, max_block_number || 0} |
||||
defp to_block_number("pending", _, pending), do: {:ok, pending} |
||||
|
||||
defp to_block_number("0x" <> number, _, _) do |
||||
case Integer.parse(number, 16) do |
||||
{integer, ""} -> {:ok, integer} |
||||
_ -> {:error, "invalid block number"} |
||||
end |
||||
end |
||||
|
||||
defp to_block_number(number, _, _) when is_bitstring(number) do |
||||
case Integer.parse(number, 16) do |
||||
{integer, ""} -> {:ok, integer} |
||||
_ -> {:error, "invalid block number"} |
||||
end |
||||
end |
||||
|
||||
defp to_block_number(_, _, _), do: {:error, "invalid block number"} |
||||
|
||||
defp max_non_consensus_block_number(max) do |
||||
case Chain.max_non_consensus_block_number(max) do |
||||
{:ok, number} -> number |
||||
_ -> nil |
||||
end |
||||
end |
||||
|
||||
defp max_consensus_block_number do |
||||
case Chain.max_consensus_block_number() do |
||||
{:ok, number} -> number |
||||
_ -> nil |
||||
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), |
||||
{:correct_arity, true} <- |
||||
{:correct_arity, :erlang.function_exported(__MODULE__, action, Enum.count(params))} do |
||||
apply(__MODULE__, action, params) |
||||
else |
||||
{:correct_arity, _} -> |
||||
{:error, "Incorrect number of params."} |
||||
|
||||
_ -> |
||||
{: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 |
||||
|
||||
defp get_action(action) do |
||||
case Map.get(@methods, action) do |
||||
%{action: action} -> |
||||
{:ok, action} |
||||
|
||||
_ -> |
||||
:error |
||||
end |
||||
end |
||||
|
||||
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,34 @@ |
||||
<section class="container"> |
||||
<div class="card"> |
||||
<div class="card-body"> |
||||
<h1 class="card-title margin-bottom-sm"><%= gettext("ETH RPC API Documentation") %></h2> |
||||
<p class="api-text-monospace" data-endpoint-url="<%= BlockScoutWeb.Endpoint.url() %>/api/eth_rpc">[ <%= gettext "Base URL:" %> <%= @conn.host %>/api/eth_rpc ]</p> |
||||
<p class="card-subtitle margin-bottom-0"> |
||||
<%= gettext "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " %> |
||||
|
||||
<a href="https://github.com/ethereum/wiki/wiki/JSON-RPC"><%= gettext "here." %></a> |
||||
<%= gettext "This is useful to allow sending requests to blockscout without having to change anything about the request." %> |
||||
<%= gettext "However, in general, the" %> <%= link( |
||||
gettext("custom RPC"), |
||||
to: api_docs_path(@conn, :index) |
||||
) %> <%= gettext " is recommended." %> |
||||
<%= gettext "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." %> |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="card"> |
||||
<div class="card-body"> |
||||
<table class="table"> |
||||
<tr> |
||||
<th>Supported Method</th> |
||||
<th>Notes</th> |
||||
</tr> |
||||
<%= for {method, info} <- Map.to_list(@documentation) do %> |
||||
<tr> |
||||
<td> <a href="https://github.com/ethereum/wiki/wiki/JSON-RPC#<%= method %>"> <%= method %> </a> </td> |
||||
<td> <%= Map.get(info, :notes, "N/A") %> </td> |
||||
</tr> |
||||
<% end %> |
||||
</table> |
||||
</div> |
||||
</section> |
After Width: | Height: | Size: 2.4 KiB |
@ -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,485 @@ |
||||
defmodule BlockScoutWeb.API.RPC.EthControllerTest do |
||||
use BlockScoutWeb.ConnCase, async: false |
||||
|
||||
alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime} |
||||
alias Explorer.Repo |
||||
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_logs" do |
||||
setup do |
||||
%{ |
||||
api_params: %{ |
||||
"method" => "eth_getLogs", |
||||
"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, [%{"address" => "badhash"}])) |
||||
|> json_response(200) |
||||
|
||||
assert %{"error" => "invalid address"} = response |
||||
end |
||||
|
||||
test "address with no logs", %{conn: conn, api_params: api_params} do |
||||
insert(:block) |
||||
address = insert(:address) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params(api_params, [%{"address" => to_string(address.hash)}])) |
||||
|> json_response(200) |
||||
|
||||
assert %{"result" => []} = response |
||||
end |
||||
|
||||
test "address but no logs and no toBlock provided", %{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params(api_params, [%{"address" => to_string(address.hash)}])) |
||||
|> json_response(200) |
||||
|
||||
assert %{"result" => []} = response |
||||
end |
||||
|
||||
test "with a matching address", %{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block = insert(:block, number: 0) |
||||
|
||||
transaction = insert(:transaction, from_address: address) |> with_block(block) |
||||
insert(:log, address: address, transaction: transaction, data: "0x010101") |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash)}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert %{"result" => [%{"data" => "0x010101"}]} = response |
||||
end |
||||
|
||||
test "with a matching address and matching topic", %{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block = insert(:block, number: 0) |
||||
|
||||
transaction = insert(:transaction, from_address: address) |> with_block(block) |
||||
insert(:log, address: address, transaction: transaction, data: "0x010101", first_topic: "0x01") |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash), "topics" => ["0x01"]}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert %{"result" => [%{"data" => "0x010101"}]} = response |
||||
end |
||||
|
||||
test "with a matching address and multiple topic matches", %{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block = insert(:block, number: 0) |
||||
|
||||
transaction = insert(:transaction, from_address: address) |> with_block(block) |
||||
insert(:log, address: address, transaction: transaction, data: "0x010101", first_topic: "0x01") |
||||
insert(:log, address: address, transaction: transaction, data: "0x020202", first_topic: "0x00") |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash), "topics" => [["0x01", "0x00"]]}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x010101"}, %{"data" => "0x020202"}] = Enum.sort_by(response["result"], &Map.get(&1, "data")) |
||||
end |
||||
|
||||
test "with a matching address and multiple topic matches in different positions", %{ |
||||
conn: conn, |
||||
api_params: api_params |
||||
} do |
||||
address = insert(:address) |
||||
|
||||
block = insert(:block, number: 0) |
||||
|
||||
transaction = insert(:transaction, from_address: address) |> with_block(block) |
||||
|
||||
insert(:log, |
||||
address: address, |
||||
transaction: transaction, |
||||
data: "0x010101", |
||||
first_topic: "0x01", |
||||
second_topic: "0x02" |
||||
) |
||||
|
||||
insert(:log, address: address, transaction: transaction, data: "0x020202", first_topic: "0x01") |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash), "topics" => ["0x01", "0x02"]}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x010101"}] = response["result"] |
||||
end |
||||
|
||||
test "with a matching address and multiple topic matches in different positions and multiple matches in the second position", |
||||
%{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block = insert(:block, number: 0) |
||||
|
||||
transaction = insert(:transaction, from_address: address) |> with_block(block) |
||||
|
||||
insert(:log, |
||||
address: address, |
||||
transaction: transaction, |
||||
data: "0x010101", |
||||
first_topic: "0x01", |
||||
second_topic: "0x02" |
||||
) |
||||
|
||||
insert(:log, |
||||
address: address, |
||||
transaction: transaction, |
||||
data: "0x020202", |
||||
first_topic: "0x01", |
||||
second_topic: "0x03" |
||||
) |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash), "topics" => ["0x01", ["0x02", "0x03"]]}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x010101"}, %{"data" => "0x020202"}] = Enum.sort_by(response["result"], &Map.get(&1, "data")) |
||||
end |
||||
|
||||
test "with a block range filter", |
||||
%{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block1 = insert(:block, number: 0) |
||||
block2 = insert(:block, number: 1) |
||||
block3 = insert(:block, number: 2) |
||||
block4 = insert(:block, number: 3) |
||||
|
||||
transaction1 = insert(:transaction, from_address: address) |> with_block(block1) |
||||
transaction2 = insert(:transaction, from_address: address) |> with_block(block2) |
||||
transaction3 = insert(:transaction, from_address: address) |> with_block(block3) |
||||
transaction4 = insert(:transaction, from_address: address) |> with_block(block4) |
||||
|
||||
insert(:log, address: address, transaction: transaction1, data: "0x010101") |
||||
|
||||
insert(:log, address: address, transaction: transaction2, data: "0x020202") |
||||
|
||||
insert(:log, address: address, transaction: transaction3, data: "0x030303") |
||||
|
||||
insert(:log, address: address, transaction: transaction4, data: "0x040404") |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => 1, "toBlock" => 2}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x020202"}, %{"data" => "0x030303"}] = Enum.sort_by(response["result"], &Map.get(&1, "data")) |
||||
end |
||||
|
||||
test "with a block hash filter", |
||||
%{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block1 = insert(:block, number: 0) |
||||
block2 = insert(:block, number: 1) |
||||
block3 = insert(:block, number: 2) |
||||
|
||||
transaction1 = insert(:transaction, from_address: address) |> with_block(block1) |
||||
transaction2 = insert(:transaction, from_address: address) |> with_block(block2) |
||||
transaction3 = insert(:transaction, from_address: address) |> with_block(block3) |
||||
|
||||
insert(:log, address: address, transaction: transaction1, data: "0x010101") |
||||
|
||||
insert(:log, address: address, transaction: transaction2, data: "0x020202") |
||||
|
||||
insert(:log, address: address, transaction: transaction3, data: "0x030303") |
||||
|
||||
params = params(api_params, [%{"address" => to_string(address.hash), "blockHash" => to_string(block2.hash)}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x020202"}] = response["result"] |
||||
end |
||||
|
||||
test "with an earliest block filter", |
||||
%{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block1 = insert(:block, number: 0) |
||||
block2 = insert(:block, number: 1) |
||||
block3 = insert(:block, number: 2) |
||||
|
||||
transaction1 = insert(:transaction, from_address: address) |> with_block(block1) |
||||
transaction2 = insert(:transaction, from_address: address) |> with_block(block2) |
||||
transaction3 = insert(:transaction, from_address: address) |> with_block(block3) |
||||
|
||||
insert(:log, address: address, transaction: transaction1, data: "0x010101") |
||||
|
||||
insert(:log, address: address, transaction: transaction2, data: "0x020202") |
||||
|
||||
insert(:log, address: address, transaction: transaction3, data: "0x030303") |
||||
|
||||
params = |
||||
params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => "earliest", "toBlock" => "earliest"}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x010101"}] = response["result"] |
||||
end |
||||
|
||||
test "with a pending block filter", |
||||
%{conn: conn, api_params: api_params} do |
||||
address = insert(:address) |
||||
|
||||
block1 = insert(:block, number: 0) |
||||
block2 = insert(:block, number: 1) |
||||
block3 = insert(:block, number: 2) |
||||
|
||||
transaction1 = insert(:transaction, from_address: address) |> with_block(block1) |
||||
transaction2 = insert(:transaction, from_address: address) |> with_block(block2) |
||||
transaction3 = insert(:transaction, from_address: address) |> with_block(block3) |
||||
|
||||
insert(:log, address: address, transaction: transaction1, data: "0x010101") |
||||
|
||||
insert(:log, address: address, transaction: transaction2, data: "0x020202") |
||||
|
||||
insert(:log, address: address, transaction: transaction3, data: "0x030303") |
||||
|
||||
changeset = Ecto.Changeset.change(block3, %{consensus: false}) |
||||
|
||||
Repo.update!(changeset) |
||||
|
||||
params = |
||||
params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => "pending", "toBlock" => "pending"}]) |
||||
|
||||
assert response = |
||||
conn |
||||
|> post("/api/eth_rpc", params) |
||||
|> json_response(200) |
||||
|
||||
assert [%{"data" => "0x030303"}] = response["result"] |
||||
end |
||||
end |
||||
|
||||
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 |