From 8a79923fcb685ecd15399e1bcdf2cf3d93778ba9 Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Wed, 10 Oct 2018 17:04:43 -0400 Subject: [PATCH] account#txlist API endpoint takes timestamp range Why: * Per an API user's request, we'd like to support a way for users to get a list of transactions for a given address and a timestamp range. - Example usage: ``` /api?module=account&action=txlist&address={addressHash}&starttimestamp=1539043200&endtimestamp=1539205684 ``` * Issue link: n/a This change addresses the need by: * Editing `optional_params/1` in `API.RPC.AddressController` to support 'starttimestamp' and 'endtimestamp' as optional params. * Editing `Explorer.Etherscan.list_transactions/3` to support `start_timestamp` and `end_timestamp` as options to filter by. * Editing api docs page for `account#txlist` to include the two new optional params added in this commit, 'starttimestamp` and `endtimestamp`. --- .../controllers/api/rpc/address_controller.ex | 24 +++ .../lib/block_scout_web/etherscan.ex | 10 ++ .../api/rpc/address_controller_test.exs | 156 +++++++++++++++++- apps/explorer/lib/explorer/etherscan.ex | 18 +- .../explorer/test/explorer/etherscan_test.exs | 35 ++++ 5 files changed, 239 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index fbad91a77f..0ef38a8699 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -178,6 +178,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do |> put_start_block(params) |> put_end_block(params) |> put_filter_by(params) + |> put_start_timestamp(params) + |> put_end_timestamp(params) end @doc """ @@ -363,6 +365,28 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end + defp put_start_timestamp(options, params) do + with %{"starttimestamp" => starttimestamp_param} <- params, + {unix_timestamp, ""} <- Integer.parse(starttimestamp_param), + {:ok, start_timestamp} <- DateTime.from_unix(unix_timestamp) do + Map.put(options, :start_timestamp, start_timestamp) + else + _ -> + options + end + end + + defp put_end_timestamp(options, params) do + with %{"endtimestamp" => endtimestamp_param} <- params, + {unix_timestamp, ""} <- Integer.parse(endtimestamp_param), + {:ok, end_timestamp} <- DateTime.from_unix(unix_timestamp) do + Map.put(options, :end_timestamp, end_timestamp) + else + _ -> + options + end + end + defp list_transactions(address_hash, options) do case Etherscan.list_transactions(address_hash, options) do [] -> {:error, :not_found} diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index b03eb43618..033be85e52 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -896,6 +896,16 @@ defmodule BlockScoutWeb.Etherscan do it returns transactions that match to, from, or contract address. Available values: to, from """ + }, + %{ + key: "starttimestamp", + type: "unix timestamp", + description: "Represents the starting block timestamp." + }, + %{ + key: "endtimestamp", + type: "unix timestamp", + description: "Represents the ending block timestamp." } ], responses: [ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index b37c5a1c03..e576d5a36a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -986,6 +986,145 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert response["message"] == "OK" end + test "with starttimestamp and endtimestamp params", %{conn: conn} do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -6) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -1) + blocks1 = insert_list(2, :block, timestamp: timestamp1) + blocks2 = [third_block, fourth_block] = insert_list(2, :block, timestamp: timestamp2) + blocks3 = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + start_timestamp = now |> Timex.shift(hours: -4) |> Timex.to_unix() + end_timestamp = now |> Timex.shift(hours: -2) |> Timex.to_unix() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "starttimestamp" => "#{start_timestamp}", + "endtimestamp" => "#{end_timestamp}" + } + + expected_block_numbers = [ + "#{third_block.number}", + "#{fourth_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with starttimestamp but without endtimestamp", %{conn: conn} do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -6) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -1) + blocks1 = insert_list(2, :block, timestamp: timestamp1) + blocks2 = [third_block, fourth_block] = insert_list(2, :block, timestamp: timestamp2) + blocks3 = [fifth_block, sixth_block] = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + start_timestamp = now |> Timex.shift(hours: -4) |> Timex.to_unix() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "starttimestamp" => "#{start_timestamp}" + } + + expected_block_numbers = [ + "#{third_block.number}", + "#{fourth_block.number}", + "#{fifth_block.number}", + "#{sixth_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 8 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with endtimestamp but without starttimestamp", %{conn: conn} do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -6) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -1) + blocks1 = [first_block, second_block] = insert_list(2, :block, timestamp: timestamp1) + blocks2 = insert_list(2, :block, timestamp: timestamp2) + blocks3 = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + end_timestamp = now |> Timex.shift(hours: -5) |> Timex.to_unix() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "endtimestamp" => "#{end_timestamp}" + } + + expected_block_numbers = [ + "#{first_block.number}", + "#{second_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + end + test "with filterby=to option", %{conn: conn} do block = insert(:block) address = insert(:address) @@ -2017,17 +2156,24 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "page" => "1", # page size "offset" => "2", - "filterby" => "to" + "filterby" => "to", + "starttimestamp" => "1539186474", + "endtimestamp" => "1539186474" } optional_params = AddressController.optional_params(params) + # 1539186474 equals "2018-10-10 15:47:54Z" + {:ok, expected_timestamp, _} = DateTime.from_iso8601("2018-10-10 15:47:54Z") + assert optional_params.page_number == 1 assert optional_params.page_size == 2 assert optional_params.order_by_direction == :asc assert optional_params.start_block == 100 assert optional_params.end_block == 120 assert optional_params.filter_by == "to" + assert optional_params.start_timestamp == expected_timestamp + assert optional_params.end_timestamp == expected_timestamp end test "'sort' values can be 'asc' or 'desc'" do @@ -2076,7 +2222,9 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "endblock" => "invalid", "sort" => "invalid", "page" => "invalid", - "offset" => "invalid" + "offset" => "invalid", + "starttimestamp" => "invalid", + "endtimestamp" => "invalid" } assert AddressController.optional_params(params1) == %{} @@ -2086,7 +2234,9 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "endblock" => "10", "sort" => "invalid", "page" => "invalid", - "offset" => "invalid" + "offset" => "invalid", + "starttimestamp" => "invalid", + "endtimestamp" => "invalid" } optional_params = AddressController.optional_params(params2) diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 7a0791500a..54b25f3831 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -16,7 +16,9 @@ defmodule Explorer.Etherscan do page_number: 1, page_size: 10_000, start_block: nil, - end_block: nil + end_block: nil, + start_timestamp: nil, + end_timestamp: nil } @doc """ @@ -289,6 +291,8 @@ defmodule Explorer.Etherscan do |> where_address_match(address_hash, options) |> where_start_block_match(options) |> where_end_block_match(options) + |> where_start_timestamp_match(options) + |> where_end_timestamp_match(options) |> Repo.all() end @@ -365,6 +369,18 @@ defmodule Explorer.Etherscan do where(query, [..., block], block.number <= ^end_block) end + defp where_start_timestamp_match(query, %{start_timestamp: nil}), do: query + + defp where_start_timestamp_match(query, %{start_timestamp: start_timestamp}) do + where(query, [..., block], ^start_timestamp <= block.timestamp) + end + + defp where_end_timestamp_match(query, %{end_timestamp: nil}), do: query + + defp where_end_timestamp_match(query, %{end_timestamp: end_timestamp}) do + where(query, [..., block], block.timestamp <= ^end_timestamp) + end + defp where_contract_address_match(query, nil), do: query defp where_contract_address_match(query, contract_address_hash) do diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index 021f7bd9cc..b192eebf5b 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -344,6 +344,41 @@ defmodule Explorer.EtherscanTest do end end + test "with start and end timestamp options" do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -1) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -6) + blocks1 = insert_list(2, :block, timestamp: timestamp1) + blocks2 = [third_block, fourth_block] = insert_list(2, :block, timestamp: timestamp2) + blocks3 = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + start_timestamp = Timex.shift(now, hours: -4) + end_timestamp = Timex.shift(now, hours: -2) + + options = %{ + start_timestamp: start_timestamp, + end_timestamp: end_timestamp + } + + found_transactions = Etherscan.list_transactions(address.hash, options) + + expected_block_numbers = [third_block.number, fourth_block.number] + + assert length(found_transactions) == 4 + + for transaction <- found_transactions do + assert transaction.block_number in expected_block_numbers + end + end + test "with filter_by: 'to' option with one matching transaction" do address = insert(:address) contract_address = insert(:contract_address)