Merge pull request #616 from poanetwork/sa-api-account-getminedblocks

Add account#getminedblocks API endpoint
pull/629/head
Sebastian Abondano 6 years ago committed by GitHub
commit 8bdf93386d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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
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 def optional_params(params) do
%{} %{}
|> put_order_by_direction(params) |> put_order_by_direction(params)
@ -247,4 +266,11 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
token_transfers -> {:ok, token_transfers} token_transfers -> {:ok, token_transfers}
end end
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 end

@ -130,6 +130,24 @@ defmodule BlockScoutWeb.Etherscan do
"result" => [] "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 %{ @logs_getlogs_example_value %{
"status" => "1", "status" => "1",
"message" => "OK", "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 %{ @token_transfer_model %{
name: "TokenTransfer", name: "TokenTransfer",
fields: %{ 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 %{ @logs_getlogs_action %{
name: "getLogs", name: "getLogs",
description: "Get event logs for an address and/or topics. Up to a maximum of 1,000 event logs.", 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_balancemulti_action,
@account_txlist_action, @account_txlist_action,
@account_txlistinternal_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) RPCView.render("show.json", data: data)
end 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 def render("error.json", assigns) do
RPCView.render("error.json", assigns) RPCView.render("error.json", assigns)
end end
@ -105,4 +110,12 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
"confirmations" => to_string(token_transfer.confirmations) "confirmations" => to_string(token_transfer.confirmations)
} }
end end
defp prepare_block(block) do
%{
"blockNumber" => to_string(block.number),
"timeStamp" => to_string(block.timestamp),
"blockReward" => to_string(block.reward.value)
}
end
end end

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
use BlockScoutWeb.ConnCase use BlockScoutWeb.ConnCase
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Transaction alias Explorer.Chain.{Transaction, Wei}
alias BlockScoutWeb.API.RPC.AddressController alias BlockScoutWeb.API.RPC.AddressController
describe "balance" do describe "balance" do
@ -1331,6 +1331,188 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
end end
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 describe "optional_params/1" do
test "includes valid optional params in the required format" do test "includes valid optional params in the required format" do
params = %{ params = %{

@ -7,7 +7,8 @@ defmodule Explorer.Etherscan do
alias Explorer.Etherscan.Logs alias Explorer.Etherscan.Logs
alias Explorer.{Repo, Chain} 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 %{ @default_options %{
order_by_direction: :asc, order_by_direction: :asc,
@ -111,6 +112,50 @@ defmodule Explorer.Etherscan do
end end
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( @transaction_fields ~w(
block_hash block_hash
block_number block_number

@ -4,7 +4,7 @@ defmodule Explorer.EtherscanTest do
import Explorer.Factory import Explorer.Factory
alias Explorer.{Etherscan, Chain} alias Explorer.{Etherscan, Chain}
alias Explorer.Chain.Transaction alias Explorer.Chain.{Transaction, Wei}
describe "list_transactions/2" do describe "list_transactions/2" do
test "with empty db" 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 assert found_token_transfer.token_contract_address_hash == contract_address.hash
end end
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 end

Loading…
Cancel
Save