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 e17cfbdd52..141caddb09 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 @@ -118,6 +118,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do |> put_pagination_options(params) |> put_start_block(params) |> put_end_block(params) + |> put_filter_by(params) end defp fetch_address(params) do @@ -246,6 +247,16 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end + defp put_filter_by(options, params) do + case params do + %{"filterby" => filter_by} when filter_by in ["to"] -> + Map.put(options, :filter_by, filter_by) + + _ -> + 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 4905a0b334..8e466ef95e 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -603,6 +603,15 @@ defmodule BlockScoutWeb.Etherscan do type: "integer", description: "A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction." + }, + %{ + key: "filterby", + type: "string", + description: """ + A string representing the field to filter by. If none is given + it returns transactions that match to, from, or contract address. + Available values: to + """ } ], 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 ce605a6db9..e07d8cce62 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 @@ -1522,7 +1522,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do # page number "page" => "1", # page size - "offset" => "2" + "offset" => "2", + "filterby" => "to" } optional_params = AddressController.optional_params(params) @@ -1532,6 +1533,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do 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" end test "'sort' values can be 'asc' or 'desc'" do @@ -1552,6 +1554,18 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert AddressController.optional_params(params3) == %{} end + test "'filterby' value can only be 'to'" do + params1 = %{"filterby" => "to"} + + optional_params = AddressController.optional_params(params1) + + assert optional_params.filter_by == "to" + + params2 = %{"filterby" => "invalid"} + + assert AddressController.optional_params(params2) == %{} + end + test "only includes optional params when they're given" do assert AddressController.optional_params(%{}) == %{} end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index a1e7082ab6..c86a056a02 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -3,7 +3,7 @@ defmodule Explorer.Etherscan do The etherscan context. """ - import Ecto.Query, only: [from: 2, where: 3] + import Ecto.Query, only: [from: 2, where: 3, or_where: 3] alias Explorer.Etherscan.Logs alias Explorer.{Repo, Chain} @@ -30,6 +30,10 @@ defmodule Explorer.Etherscan do @doc """ Gets a list of transactions for a given `t:Explorer.Chain.Hash.Address.t/0`. + If `filter_by: "to"` is given as an option, address matching is only done + against the `to_address_hash` column. If not, `to_address_hash`, + `from_address_hash`, and `created_contract_address_hash` are all considered. + """ @spec list_transactions(Hash.Address.t()) :: [map()] def list_transactions( @@ -179,9 +183,6 @@ defmodule Explorer.Etherscan do from( t in Transaction, inner_join: b in assoc(t, :block), - where: t.to_address_hash == ^address_hash, - or_where: t.from_address_hash == ^address_hash, - or_where: t.created_contract_address_hash == ^address_hash, order_by: [{^options.order_by_direction, t.block_number}], limit: ^options.page_size, offset: ^offset(options), @@ -193,11 +194,23 @@ defmodule Explorer.Etherscan do ) query + |> where_address_match(address_hash, options) |> where_start_block_match(options) |> where_end_block_match(options) |> Repo.all() end + defp where_address_match(query, address_hash, %{filter_by: "to"}) do + where(query, [t], t.to_address_hash == ^address_hash) + end + + defp where_address_match(query, address_hash, _) do + query + |> where([t], t.to_address_hash == ^address_hash) + |> or_where([t], t.from_address_hash == ^address_hash) + |> or_where([t], t.created_contract_address_hash == ^address_hash) + end + @token_transfer_fields ~w( token_contract_address_hash transaction_hash diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index cd419c7891..175a09800a 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -343,6 +343,42 @@ defmodule Explorer.EtherscanTest 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) + + :transaction + |> insert(to_address: address) + |> with_block() + + :transaction + |> insert(from_address: address, to_address: nil) + |> with_contract_creation(contract_address) + |> with_block() + + options = %{filter_by: "to"} + + found_transactions = Etherscan.list_transactions(address.hash, options) + + assert length(found_transactions) == 1 + end + + test "with filter_by: 'to' option with non-matching transaction" do + address = insert(:address) + contract_address = insert(:contract_address) + + :transaction + |> insert(from_address: address, to_address: nil) + |> with_contract_creation(contract_address) + |> with_block() + + options = %{filter_by: "to"} + + found_transactions = Etherscan.list_transactions(address.hash, options) + + assert length(found_transactions) == 0 + end end describe "list_internal_transactions/1" do