Merge pull request #3750 from blockscout/vb-getblocknobytime-api-endpoint

getblocknobytime block module API endpoint
pull/3761/head
Victor Baranov 4 years ago committed by GitHub
commit 3cc73bf223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 14
      apps/block_scout_web/assets/js/lib/try_api.js
  3. 22
      apps/block_scout_web/lib/block_scout_web/chain.ex
  4. 25
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex
  5. 66
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  6. 8
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex
  7. 133
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs
  8. 56
      apps/explorer/lib/explorer/chain.ex

@ -1,6 +1,7 @@
## Current
### Features
- [#3750](https://github.com/blockscout/blockscout/pull/3750) - getblocknobytime block module API endpoint
### Fixes
- [#3748](https://github.com/blockscout/blockscout/pull/3748) - Skip null topics in eth_getLogs API endpoint

@ -44,7 +44,7 @@ function handleSuccess (query, xhr, clickedButton) {
const requestUrl = $(`[data-selector="${module}-${action}-request-url"]`)[0]
const code = $(`[data-selector="${module}-${action}-server-response-code"]`)[0]
const body = $(`[data-selector="${module}-${action}-server-response-body"]`)[0]
const url = composeRequestUrl(query)
const url = composeRequestUrl(escapeHtml(query))
curl.innerHTML = composeCurlCommand(url)
requestUrl.innerHTML = url
@ -56,6 +56,18 @@ function handleSuccess (query, xhr, clickedButton) {
clickedButton.prop('disabled', false)
}
function escapeHtml (text) {
var map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, function (m) { return map[m] })
}
// Show 'Try it out' UI for a module/action.
$('button[data-selector*="btn-try-api"]').click(event => {
const clickedButton = $(event.target)

@ -205,6 +205,28 @@ defmodule BlockScoutWeb.Chain do
end
end
def param_to_block_timestamp(timestamp_string) when is_binary(timestamp_string) do
case Integer.parse(timestamp_string) do
{temstamp_int, ""} ->
timestamp =
temstamp_int
|> DateTime.from_unix!(:second)
{:ok, timestamp}
_ ->
{:error, :invalid_timestamp}
end
end
def param_to_block_closest(closest) when is_binary(closest) do
case closest do
"before" -> {:ok, :before}
"after" -> {:ok, :after}
_ -> {:error, :invalid_closest}
end
end
def split_list_by_page(list_plus_one), do: Enum.split(list_plus_one, @page_size)
defp address_from_param(param) do

@ -24,6 +24,31 @@ defmodule BlockScoutWeb.API.RPC.BlockController do
end
end
def getblocknobytime(conn, params) do
with {:timestamp_param, {:ok, unsafe_timestamp}} <- {:timestamp_param, Map.fetch(params, "timestamp")},
{:closest_param, {:ok, unsafe_closest}} <- {:closest_param, Map.fetch(params, "closest")},
{:ok, timestamp} <- ChainWeb.param_to_block_timestamp(unsafe_timestamp),
{:ok, closest} <- ChainWeb.param_to_block_closest(unsafe_closest),
{:ok, block_number} <- Chain.timestamp_to_block_number(timestamp, closest) do
render(conn, block_number: block_number)
else
{:timestamp_param, :error} ->
render(conn, :error, error: "Query parameter 'timestamp' is required")
{:closest_param, :error} ->
render(conn, :error, error: "Query parameter 'closest' is required")
{:error, :invalid_timestamp} ->
render(conn, :error, error: "Invalid `timestamp` param")
{:error, :invalid_closest} ->
render(conn, :error, error: "Invalid `closest` param")
{:error, :not_found} ->
render(conn, :error, error: "Block does not exist")
end
end
def eth_block_number(conn, params) do
id = Map.get(params, "id", 1)
max_block_number = BlockNumber.get_max()

@ -343,6 +343,20 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil
}
@block_getblocknobytime_example_value %{
"status" => "1",
"message" => "OK",
"result" => %{
"blockNumber" => "2165403"
}
}
@block_getblocknobytime_example_value_error %{
"status" => "0",
"message" => "Invalid params",
"result" => nil
}
@block_eth_block_number_example_value %{
"jsonrpc" => "2.0",
"result" => "0xb33bf1",
@ -936,6 +950,13 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@block_no_model %{
name: "BlockNo",
fields: %{
blockNumber: @block_number_type
}
}
@account_model %{
name: "Account",
fields: %{
@ -2112,6 +2133,49 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@block_getblocknobytime_action %{
name: "getblocknobytime",
description: "Get Block Number by Timestamp.",
required_params: [
%{
key: "timestamp",
placeholder: "blockTimestamp",
type: "integer",
description: "A nonnegative integer that represents the block timestamp (Unix timestamp in seconds)."
},
%{
key: "closest",
placeholder: "before/after",
type: "string",
description: "Direction to find the closest block number to given timestamp. Available values: before/after."
}
],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@block_getblocknobytime_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "model",
model: @block_no_model
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@block_getblocknobytime_example_value_error)
}
]
}
@contract_listcontracts_action %{
name: "listcontracts",
description: """
@ -2544,7 +2608,7 @@ defmodule BlockScoutWeb.Etherscan do
@block_module %{
name: "block",
actions: [@block_getblockreward_action, @block_eth_block_number_action]
actions: [@block_getblockreward_action, @block_getblocknobytime_action, @block_eth_block_number_action]
}
@contract_module %{

@ -23,6 +23,14 @@ defmodule BlockScoutWeb.API.RPC.BlockView do
RPCView.render("show.json", data: data)
end
def render("getblocknobytime.json", %{block_number: block_number}) do
data = %{
"blockNumber" => to_string(block_number)
}
RPCView.render("show.json", data: data)
end
def render("eth_block_number.json", %{number: number, id: id}) do
result = EthRPC.encode_quantity(number)

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Hash, Wei}
alias BlockScoutWeb.Chain
describe "getblockreward" do
test "with missing block number", %{conn: conn} do
@ -82,6 +83,138 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do
end
end
describe "getblocknobytime" do
test "with missing timestamp param", %{conn: conn} do
response =
conn
|> get("/api", %{"module" => "block", "action" => "getblocknobytime", "closest" => "after"})
|> json_response(200)
assert response["message"] =~ "Query parameter 'timestamp' is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
schema = resolve_schema()
assert :ok = ExJsonSchema.Validator.validate(schema, response)
end
test "with missing closest param", %{conn: conn} do
response =
conn
|> get("/api", %{"module" => "block", "action" => "getblocknobytime", "timestamp" => "1617019505"})
|> json_response(200)
assert response["message"] =~ "Query parameter 'closest' is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
schema = resolve_schema()
assert :ok = ExJsonSchema.Validator.validate(schema, response)
end
test "with an invalid timestamp param", %{conn: conn} do
response =
conn
|> get("/api", %{
"module" => "block",
"action" => "getblocknobytime",
"timestamp" => "invalid",
"closest" => " before"
})
|> json_response(200)
assert response["message"] =~ "Invalid `timestamp` param"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
schema = resolve_schema()
assert :ok = ExJsonSchema.Validator.validate(schema, response)
end
test "with an invalid closest param", %{conn: conn} do
response =
conn
|> get("/api", %{
"module" => "block",
"action" => "getblocknobytime",
"timestamp" => "1617019505",
"closest" => "invalid"
})
|> json_response(200)
assert response["message"] =~ "Invalid `closest` param"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
schema = resolve_schema()
assert :ok = ExJsonSchema.Validator.validate(schema, response)
end
test "with valid params and before", %{conn: conn} do
timestamp_string = "1617020209"
{:ok, timestamp} = Chain.param_to_block_timestamp(timestamp_string)
block = insert(:block, timestamp: timestamp)
{timestamp_int, _} = Integer.parse(timestamp_string)
timestamp_in_the_future_str =
(timestamp_int + 1)
|> to_string()
expected_result = %{
"blockNumber" => "#{block.number}"
}
assert response =
conn
|> get("/api", %{
"module" => "block",
"action" => "getblocknobytime",
"timestamp" => "#{timestamp_in_the_future_str}",
"closest" => "before"
})
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
schema = resolve_schema()
assert :ok = ExJsonSchema.Validator.validate(schema, response)
end
test "with valid params and after", %{conn: conn} do
timestamp_string = "1617020209"
{:ok, timestamp} = Chain.param_to_block_timestamp(timestamp_string)
block = insert(:block, timestamp: timestamp)
{timestamp_int, _} = Integer.parse(timestamp_string)
timestamp_in_the_past_str =
(timestamp_int - 1)
|> to_string()
expected_result = %{
"blockNumber" => "#{block.number}"
}
assert response =
conn
|> get("/api", %{
"module" => "block",
"action" => "getblocknobytime",
"timestamp" => "#{timestamp_in_the_past_str}",
"closest" => "after"
})
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
schema = resolve_schema()
assert :ok = ExJsonSchema.Validator.validate(schema, response)
end
end
defp resolve_schema() do
ExJsonSchema.Schema.resolve(%{
"type" => "object",

@ -2640,6 +2640,62 @@ defmodule Explorer.Chain do
end
end
@spec timestamp_to_block_number(DateTime.t(), :before | :after) :: {:ok, Block.block_number()} | {:error, :not_found}
def timestamp_to_block_number(given_timestamp, closest) do
{:ok, t} = Timex.format(given_timestamp, "%Y-%m-%d %H:%M:%S", :strftime)
inner_query =
from(
block in Block,
where: block.consensus == true,
where:
fragment("? <= TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS') + (1 * interval '1 minute')", block.timestamp, ^t),
where:
fragment("? >= TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS') - (1 * interval '1 minute')", block.timestamp, ^t)
)
query =
from(
block in subquery(inner_query),
select: block,
order_by:
fragment("abs(extract(epoch from (? - TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS'))))", block.timestamp, ^t),
limit: 1
)
query
|> Repo.one()
|> case do
nil ->
{:error, :not_found}
%{:number => number, :timestamp => timestamp} ->
block_number = get_block_number_based_on_closest(closest, timestamp, given_timestamp, number)
{:ok, block_number}
end
end
defp get_block_number_based_on_closest(closest, timestamp, given_timestamp, number) do
case closest do
:before ->
if DateTime.compare(timestamp, given_timestamp) == :lt ||
DateTime.compare(timestamp, given_timestamp) == :eq do
number
else
number - 1
end
:after ->
if DateTime.compare(timestamp, given_timestamp) == :lt ||
DateTime.compare(timestamp, given_timestamp) == :eq do
number + 1
else
number
end
end
end
@doc """
Count of pending `t:Explorer.Chain.Transaction.t/0`.

Loading…
Cancel
Save