Skip to content

Commit

Permalink
feat: eth_get_balance rpc endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jun 7, 2019
1 parent 1128c0a commit e40027e
Show file tree
Hide file tree
Showing 11 changed files with 556 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- [#2037](https://github.com/poanetwork/blockscout/pull/2037) - add address logs search functionality
- [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async
- [#2064](https://github.com/poanetwork/blockscout/pull/2064) - feat: add fields to tx apis, small cleanups
- [#2100](https://github.com/poanetwork/blockscout/pull/2100) - feat: eth_get_balance rpc endpoint

### Fixes
- [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,35 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
|> render(:listaccounts, %{accounts: accounts})
end

def eth_get_balance(conn, params) do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:block_param, {:ok, block}} <- {:block_param, fetch_block_param(params)},
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address_hash, block)} do
render(conn, :eth_get_balance, %{balance: Wei.hex_format(balance)})
else
{:address_param, :error} ->
conn
|> put_status(400)
|> render(:eth_get_balance_error, %{message: "Query parameter 'address' is required"})

{:format, :error} ->
conn
|> put_status(400)
|> render(:eth_get_balance_error, %{error: "Invalid address hash"})

{:block_param, :error} ->
conn
|> put_status(400)
|> render(:eth_get_balance_error, %{error: "Invalid block"})

{:balance, {:error, :not_found}} ->
conn
|> put_status(404)
|> render(:eth_get_balance_error, %{error: "Balance not found"})
end
end

def balance(conn, params, template \\ :balance) do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hashes}} <- to_address_hashes(address_param) do
Expand Down Expand Up @@ -217,6 +246,20 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
{:required_params, result}
end

defp fetch_block_param(%{"block" => "latest"}), do: {:ok, :latest}
defp fetch_block_param(%{"block" => "earliest"}), do: {:ok, :earliest}
defp fetch_block_param(%{"block" => "pending"}), do: {:ok, :pending}

defp fetch_block_param(%{"block" => string_integer}) when is_bitstring(string_integer) do
case Integer.parse(string_integer) do
{integer, ""} -> {:ok, integer}
_ -> :error
end
end

defp fetch_block_param(%{"block" => _block}), do: :error
defp fetch_block_param(_), do: {:ok, :latest}

defp to_valid_format(params, :tokenbalance) do
result =
with {:ok, contract_address_hash} <- to_address_hash(params, "contractaddress"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/etherscan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ defmodule BlockScoutWeb.Etherscan do
"result" => []
}

@account_eth_get_balance_example_value %{
"jsonrpc" => "2.0",
"result" => "0x0234c8a3397aab58",
"id" => 1
}

@account_tokentx_example_value %{
"status" => "1",
"message" => "OK",
Expand Down Expand Up @@ -1028,6 +1034,49 @@ defmodule BlockScoutWeb.Etherscan do
}
}

@account_eth_get_balance_action %{
name: "eth_get_balance",
description:
"Mimics Ethereum JSON RPC's eth_getBalance. Returns the balance as of the provided block (defaults to latest)",
required_params: [
%{
key: "address",
placeholder: "addressHash",
type: "string",
description: "The address of the account."
}
],
optional_params: [
%{
key: "block",
placeholder: "block",
type: "string",
description: """
Either the block number as a string, or one of latest, earliest or pending
latest will be the latest balance in a *consensus* block.
earliest will be the first recorded balance for the address.
pending will be the latest balance in consensus *or* nonconcensus blocks.
"""
}
],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@account_eth_get_balance_example_value),
model: %{
name: "Result",
fields: %{
jsonrpc: @jsonrpc_version_type,
id: @id_type,
result: @hex_number_type
}
}
}
]
}

@account_balance_action %{
name: "balance",
description: """
Expand Down Expand Up @@ -2203,6 +2252,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_module %{
name: "account",
actions: [
@account_eth_get_balance_action,
@account_balance_action,
@account_balancemulti_action,
@account_txlist_action,
Expand Down
2 changes: 2 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ defmodule BlockScoutWeb.Router do

alias BlockScoutWeb.API.RPC

post("/eth_rpc", EthController, :eth_request)

forward("/", RPCTranslator, %{
"block" => RPC.BlockController,
"account" => RPC.AddressController,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule BlockScoutWeb.API.RPC.AddressView do
use BlockScoutWeb, :view

alias BlockScoutWeb.API.RPC.RPCView
alias BlockScoutWeb.API.RPC.{EthRPCView, RPCView}

def render("listaccounts.json", %{accounts: accounts}) do
accounts = Enum.map(accounts, &prepare_account/1)
Expand Down Expand Up @@ -51,6 +51,10 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
RPCView.render("show.json", data: data)
end

def render("eth_get_balance_error.json", %{error: message}) do
EthRPCView.render("error.json", %{error: message, id: 0})
end

def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,48 @@ defmodule BlockScoutWeb.API.RPC.EthRPCView do
}
end

def render("response.json", %{response: %{error: error, id: id}}) do
%__MODULE__{
error: error,
id: id
}
end

def render("response.json", %{response: %{result: result, id: id}}) do
%__MODULE__{
result: result,
id: id
}
end

def render("responses.json", %{responses: responses}) do
Enum.map(responses, fn
%{error: error, id: id} ->
%__MODULE__{
error: error,
id: id
}

%{result: result, id: id} ->
%__MODULE__{
result: result,
id: id
}
end)
end

defimpl Poison.Encoder, for: BlockScoutWeb.API.RPC.EthRPCView do
def encode(%BlockScoutWeb.API.RPC.EthRPCView{result: result, id: id, error: error}, _options) when is_nil(error) do
result = Poison.encode!(result)

"""
{"jsonrpc":"2.0","result":"#{result}","id":#{id}}
{"jsonrpc":"2.0","result":#{result},"id":#{id}}
"""
end

def encode(%BlockScoutWeb.API.RPC.EthRPCView{id: id, error: error}, _options) do
"""
{"jsonrpc":"2.0","error": #{error},"id": #{id}}
{"jsonrpc":"2.0","error": "#{error}","id": #{id}}
"""
end
end
Expand Down
13 changes: 13 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit e40027e

Please sign in to comment.