Add account#getminedblocks API endpoint

Why:

* For API users to be able to get a list of blocks mined by a given
address.

  Example usage:
    ```
    /api?module=account&action=getminedblocks&address={addressHash}
    ```

* Issue link: https://github.com/poanetwork/blockscout/issues/138

This change addresses the need by:

* Adding `list_blocks/2` to `Explorer.Etherscan` to be get blocks as
required by the new API endpoint added in this commit.
* Adding `getminedblocks/2` action to `API.RPC.AddressController` to
handle `account#getminedblocks` API requests.
* Editing API.RPC.AddressView to render blocks as needed.
* Adding docs for `account#getminedblocks` API endpoint. API docs live
in `BlockScoutWeb.Etherscan`
pull/616/head
Sebastian Abondano 6 years ago
parent 233730abba
commit 0b42ed19f4
  1. 26
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  2. 88
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  3. 13
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  4. 184
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  5. 47
      apps/explorer/lib/explorer/etherscan.ex
  6. 191
      apps/explorer/test/explorer/etherscan_test.exs

@ -93,6 +93,25 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
end
def getminedblocks(conn, params) do
options = put_pagination_options(%{}, params)
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
{:ok, blocks} <- list_blocks(address_hash, options) do
render(conn, :getminedblocks, %{blocks: blocks})
else
{:address_param, :error} ->
render(conn, :error, error: "Query parameter 'address' is required")
{:format, :error} ->
render(conn, :error, error: "Invalid address format")
{:error, :not_found} ->
render(conn, :error, error: "No blocks found", data: [])
end
end
def optional_params(params) do
%{}
|> put_order_by_direction(params)
@ -247,4 +266,11 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
token_transfers -> {:ok, token_transfers}
end
end
defp list_blocks(address_hash, options) do
case Etherscan.list_blocks(address_hash, options) do
[] -> {:error, :not_found}
blocks -> {:ok, blocks}
end
end
end

@ -130,6 +130,24 @@ defmodule BlockScoutWeb.Etherscan do
"result" => []
}
@account_getminedblocks_example_value %{
"status" => "1",
"message" => "OK",
"result" => [
%{
"blockNumber" => "3462296",
"timeStamp" => "1491118514",
"blockReward" => "5194770940000000000"
}
]
}
@account_getminedblocks_example_value_error %{
"status" => "0",
"message" => "No blocks found",
"result" => []
}
@logs_getlogs_example_value %{
"status" => "1",
"message" => "OK",
@ -345,6 +363,23 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@block_model %{
name: "Block",
fields: %{
blockNumber: @block_number_type,
timeStamp: %{
type: "timestamp",
definition: "When the block was collated.",
example: ~s("1480072029")
},
blockReward: %{
type: "block reward",
definition: "The reward given to the miner of a block.",
example: ~s("5003251945421042780")
}
}
}
@token_transfer_model %{
name: "TokenTransfer",
fields: %{
@ -704,6 +739,56 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@account_getminedblocks_action %{
name: "getminedblocks",
description: "Get list of blocks mined by address.",
required_params: [
%{
key: "address",
placeholder: "addressHash",
type: "string",
description: "A 160-bit code used for identifying accounts."
}
],
optional_params: [
%{
key: "page",
type: "integer",
description:
"A nonnegative integer that represents the page number to be used for pagination. 'offset' must be provided in conjunction."
},
%{
key: "offset",
type: "integer",
description:
"A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction."
}
],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@account_getminedblocks_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "array",
array_type: @block_model
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@account_getminedblocks_example_value_error)
}
]
}
@logs_getlogs_action %{
name: "getLogs",
description: "Get event logs for an address and/or topics. Up to a maximum of 1,000 event logs.",
@ -906,7 +991,8 @@ defmodule BlockScoutWeb.Etherscan do
@account_balancemulti_action,
@account_txlist_action,
@account_txlistinternal_action,
@account_tokentx_action
@account_tokentx_action,
@account_getminedblocks_action
]
}

@ -38,6 +38,11 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
RPCView.render("show.json", data: data)
end
def render("getminedblocks.json", %{blocks: blocks}) do
data = Enum.map(blocks, &prepare_block/1)
RPCView.render("show.json", data: data)
end
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
@ -105,4 +110,12 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
"confirmations" => to_string(token_transfer.confirmations)
}
end
defp prepare_block(block) do
%{
"blockNumber" => to_string(block.number),
"timeStamp" => to_string(block.timestamp),
"blockReward" => to_string(block.reward.value)
}
end
end

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain
alias Explorer.Chain.Transaction
alias Explorer.Chain.{Transaction, Wei}
alias BlockScoutWeb.API.RPC.AddressController
describe "balance" do
@ -1331,6 +1331,188 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
end
end
describe "getminedblocks" do
test "with missing address hash", %{conn: conn} do
params = %{
"module" => "account",
"action" => "getminedblocks"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "'address' is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an invalid address hash", %{conn: conn} do
params = %{
"module" => "account",
"action" => "getminedblocks",
"address" => "badhash"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "Invalid address format"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an address that doesn't exist", %{conn: conn} do
params = %{
"module" => "account",
"action" => "getminedblocks",
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == []
assert response["status"] == "0"
assert response["message"] == "No blocks found"
end
test "returns all the required fields", %{conn: conn} do
%{block_range: range} = block_reward = insert(:block_reward)
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
:transaction
|> insert(gas_price: 1)
|> with_block(block, gas_used: 1)
expected_reward =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(1))
|> Wei.from(:wei)
expected_result = [
%{
"blockNumber" => to_string(block.number),
"timeStamp" => to_string(block.timestamp),
"blockReward" => to_string(expected_reward.value)
}
]
params = %{
"module" => "account",
"action" => "getminedblocks",
"address" => to_string(block.miner_hash)
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with a block with one transaction", %{conn: conn} do
%{block_range: range} = block_reward = insert(:block_reward)
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
:transaction
|> insert(gas_price: 1)
|> with_block(block, gas_used: 1)
expected_reward =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(1))
|> Wei.from(:wei)
params = %{
"module" => "account",
"action" => "getminedblocks",
"address" => to_string(block.miner_hash)
}
expected_result = [
%{
"blockNumber" => to_string(block.number),
"timeStamp" => to_string(block.timestamp),
"blockReward" => to_string(expected_reward.value)
}
]
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with pagination options", %{conn: conn} do
%{block_range: range} = block_reward = insert(:block_reward)
block_numbers = Range.new(range.from, range.to)
[block_number1, block_number2] = Enum.take(block_numbers, 2)
address = insert(:address)
block1 = insert(:block, number: block_number1, miner: address)
_block2 = insert(:block, number: block_number2, miner: address)
:transaction
|> insert(gas_price: 2)
|> with_block(block1, gas_used: 2)
expected_reward =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(4))
|> Wei.from(:wei)
params = %{
"module" => "account",
"action" => "getminedblocks",
"address" => to_string(address.hash),
# page number
"page" => "1",
# page size
"offset" => "1"
}
expected_result = [
%{
"blockNumber" => to_string(block1.number),
"timeStamp" => to_string(block1.timestamp),
"blockReward" => to_string(expected_reward.value)
}
]
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
describe "optional_params/1" do
test "includes valid optional params in the required format" do
params = %{

@ -7,7 +7,8 @@ defmodule Explorer.Etherscan do
alias Explorer.Etherscan.Logs
alias Explorer.{Repo, Chain}
alias Explorer.Chain.{Hash, InternalTransaction, Transaction}
alias Explorer.Chain.{Block, Hash, InternalTransaction, Transaction, Wei}
alias Explorer.Chain.Block.Reward
@default_options %{
order_by_direction: :asc,
@ -111,6 +112,50 @@ defmodule Explorer.Etherscan do
end
end
@doc """
Gets a list of blocks mined by `t:Explorer.Chain.Hash.Address.t/0`.
For each block it returns the block's number, timestamp, and reward.
The block reward is the sum of the following:
* Sum of the transaction fees (gas_used * gas_price) for the block
* A static reward for miner (this value may change during the life of the chain)
* The reward for uncle blocks (1/32 * static_reward * number_of_uncles)
*NOTE*
Uncles are not currently accounted for.
"""
@spec list_blocks(Hash.Address.t()) :: [map()]
def list_blocks(%Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash, options \\ %{}) do
merged_options = Map.merge(@default_options, options)
query =
from(
b in Block,
left_join: t in assoc(b, :transactions),
inner_join: r in Reward,
on: fragment("? <@ ?", b.number, r.block_range),
where: b.miner_hash == ^address_hash,
group_by: b.number,
group_by: b.timestamp,
group_by: r.reward,
limit: ^merged_options.page_size,
offset: ^offset(merged_options),
select: %{
number: b.number,
timestamp: b.timestamp,
reward: %Wei{
value: fragment("coalesce(sum(? * ?), 0) + ?", t.gas_used, t.gas_price, r.reward)
}
}
)
Repo.all(query)
end
@transaction_fields ~w(
block_hash
block_number

@ -4,7 +4,7 @@ defmodule Explorer.EtherscanTest do
import Explorer.Factory
alias Explorer.{Etherscan, Chain}
alias Explorer.Chain.Transaction
alias Explorer.Chain.{Transaction, Wei}
describe "list_transactions/2" do
test "with empty db" do
@ -800,4 +800,193 @@ defmodule Explorer.EtherscanTest do
assert found_token_transfer.token_contract_address_hash == contract_address.hash
end
end
describe "list_blocks/1" do
test "it returns all required fields" do
%{block_range: range} = block_reward = insert(:block_reward)
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
# irrelevant transaction
insert(:transaction)
:transaction
|> insert(gas_price: 1)
|> with_block(block, gas_used: 1)
expected_reward =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(1))
|> Wei.from(:wei)
expected = [
%{
number: block.number,
timestamp: block.timestamp,
reward: expected_reward
}
]
assert Etherscan.list_blocks(block.miner_hash) == expected
end
test "with block containing multiple transactions" do
%{block_range: range} = block_reward = insert(:block_reward)
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
# irrelevant transaction
insert(:transaction)
:transaction
|> insert(gas_price: 1)
|> with_block(block, gas_used: 1)
:transaction
|> insert(gas_price: 1)
|> with_block(block, gas_used: 2)
expected_reward =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(3))
|> Wei.from(:wei)
expected = [
%{
number: block.number,
timestamp: block.timestamp,
reward: expected_reward
}
]
assert Etherscan.list_blocks(block.miner_hash) == expected
end
test "with block without transactions" do
%{block_range: range} = block_reward = insert(:block_reward)
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
# irrelevant transaction
insert(:transaction)
expected = [
%{
number: block.number,
timestamp: block.timestamp,
reward: block_reward.reward
}
]
assert Etherscan.list_blocks(block.miner_hash) == expected
end
test "with multiple blocks" do
%{block_range: range} = block_reward = insert(:block_reward)
block_numbers = Range.new(range.from, range.to)
[block_number1, block_number2] = Enum.take(block_numbers, 2)
address = insert(:address)
block1 = insert(:block, number: block_number1, miner: address)
block2 = insert(:block, number: block_number2, miner: address)
# irrelevant transaction
insert(:transaction)
:transaction
|> insert(gas_price: 2)
|> with_block(block1, gas_used: 2)
:transaction
|> insert(gas_price: 2)
|> with_block(block1, gas_used: 2)
:transaction
|> insert(gas_price: 3)
|> with_block(block2, gas_used: 3)
:transaction
|> insert(gas_price: 3)
|> with_block(block2, gas_used: 3)
expected_reward1 =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(8))
|> Wei.from(:wei)
expected_reward2 =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(18))
|> Wei.from(:wei)
expected = [
%{
number: block1.number,
timestamp: block1.timestamp,
reward: expected_reward1
},
%{
number: block2.number,
timestamp: block2.timestamp,
reward: expected_reward2
}
]
assert Etherscan.list_blocks(address.hash) == expected
end
test "with pagination options" do
%{block_range: range} = block_reward = insert(:block_reward)
block_numbers = Range.new(range.from, range.to)
[block_number1, block_number2] = Enum.take(block_numbers, 2)
address = insert(:address)
block1 = insert(:block, number: block_number1, miner: address)
block2 = insert(:block, number: block_number2, miner: address)
:transaction
|> insert(gas_price: 2)
|> with_block(block1, gas_used: 2)
expected_reward =
block_reward.reward
|> Wei.to(:wei)
|> Decimal.add(Decimal.new(4))
|> Wei.from(:wei)
expected1 = [
%{
number: block1.number,
timestamp: block1.timestamp,
reward: expected_reward
}
]
expected2 = [
%{
number: block2.number,
timestamp: block2.timestamp,
reward: block_reward.reward
}
]
options1 = %{page_number: 1, page_size: 1}
options2 = %{page_number: 2, page_size: 1}
options3 = %{page_number: 3, page_size: 1}
assert Etherscan.list_blocks(address.hash, options1) == expected1
assert Etherscan.list_blocks(address.hash, options2) == expected2
assert Etherscan.list_blocks(address.hash, options3) == []
end
end
end

Loading…
Cancel
Save