commit
390624816c
@ -0,0 +1,371 @@ |
||||
defmodule Explorer.EthRPC do |
||||
@moduledoc """ |
||||
Ethreum JSON RPC methods logic implementation. |
||||
""" |
||||
|
||||
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 |
||||
""", |
||||
example: """ |
||||
{"id": 0, "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x0000000000000000000000000000000000000007", "2"]} |
||||
""" |
||||
}, |
||||
"eth_getLogs" => %{ |
||||
action: :eth_get_logs, |
||||
notes: """ |
||||
Will never return more than 1000 log entries.\n |
||||
For this reason, you can use pagination options to request the next page. Pagination options params: {"logIndex": "3D", "blockNumber": "6423AC", "transactionIndex": 53} which include parameters from the last log received from the previous request. These three parameters are required for pagination. |
||||
""", |
||||
example: """ |
||||
{"id": 0, "jsonrpc": "2.0", "method": "eth_getLogs", |
||||
"params": [ |
||||
{"address": "0xc78Be425090Dbd437532594D12267C5934Cc6c6f", |
||||
"paging_options": {"logIndex": "3D", "blockNumber": "6423AC", "transactionIndex": 53}, |
||||
"fromBlock": "earliest", |
||||
"toBlock": "latest", |
||||
"topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]} |
||||
""" |
||||
} |
||||
} |
||||
|
||||
@index_to_word %{ |
||||
0 => "first", |
||||
1 => "second", |
||||
2 => "third", |
||||
3 => "fourth" |
||||
} |
||||
|
||||
def 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 |
||||
|
||||
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), |
||||
{:ok, paging_options} <- paging_options(filter_options) do |
||||
filter = |
||||
address_or_topic_params |
||||
|> Map.put(:from_block, from_block) |
||||
|> Map.put(:to_block, to_block) |
||||
|> Map.put(:allow_non_consensus, true) |
||||
|
||||
logs = |
||||
filter |
||||
|> Logs.list_logs(paging_options) |
||||
|> Enum.map(&render_log/1) |
||||
|
||||
{:ok, logs} |
||||
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 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 paging_options(%{ |
||||
"paging_options" => %{ |
||||
"logIndex" => log_index, |
||||
"transactionIndex" => transaction_index, |
||||
"blockNumber" => block_number |
||||
} |
||||
}) |
||||
when is_integer(transaction_index) do |
||||
with {:ok, parsed_block_number} <- to_number(block_number, "invalid block number"), |
||||
{:ok, parsed_log_index} <- to_number(log_index, "invalid log index") do |
||||
{:ok, |
||||
%{ |
||||
log_index: parsed_log_index, |
||||
transaction_index: transaction_index, |
||||
block_number: parsed_block_number |
||||
}} |
||||
end |
||||
end |
||||
|
||||
defp paging_options(_), do: {:ok, nil} |
||||
|
||||
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 to_number(number, error_message) when is_bitstring(number) do |
||||
case Integer.parse(number, 16) do |
||||
{integer, ""} -> {:ok, integer} |
||||
_ -> {:error, error_message} |
||||
end |
||||
end |
||||
|
||||
defp to_number(_, error_message), do: {:error, error_message} |
||||
|
||||
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 |
||||
|
||||
def methods, do: @methods |
||||
end |
Loading…
Reference in new issue