Add filterby flag to account#txlist

Why:

* For API users to get a list of transactions in which the given address
is the 'to' in the transaction.

  Example usage:
    ```
    /api?module=account&action&txlist&address={someAddress}&filterby=to
    ```

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

This change addresses the need by:

* Editing `API.RPC.AddressController`'s `txlist/2` action to support a
`filterby` flag in which the only allowed value for now is `to`. This is
an optional parameter. When given this filters the returned transactions
to only those in which the given address matches the `to` field. When
not given, the address provided could match the to, from, or contract
address for the transaction.
* Editing `Explorer.Etherscan`'s `list_transactions/3` to support the
new `filterby` flag mentioned above.
* Editing API docs data in `BlockScoutWeb.Etherscan` to include
information about the new `filterby` flag added in this commit.
pull/629/head
Sebastian Abondano 6 years ago
parent 8bdf93386d
commit 24e5da232f
  1. 11
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  2. 9
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  3. 16
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  4. 21
      apps/explorer/lib/explorer/etherscan.ex
  5. 36
      apps/explorer/test/explorer/etherscan_test.exs

@ -118,6 +118,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
|> put_pagination_options(params) |> put_pagination_options(params)
|> put_start_block(params) |> put_start_block(params)
|> put_end_block(params) |> put_end_block(params)
|> put_filter_by(params)
end end
defp fetch_address(params) do defp fetch_address(params) do
@ -246,6 +247,16 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end end
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 defp list_transactions(address_hash, options) do
case Etherscan.list_transactions(address_hash, options) do case Etherscan.list_transactions(address_hash, options) do
[] -> {:error, :not_found} [] -> {:error, :not_found}

@ -603,6 +603,15 @@ defmodule BlockScoutWeb.Etherscan do
type: "integer", type: "integer",
description: description:
"A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction." "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: [ responses: [

@ -1522,7 +1522,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
# page number # page number
"page" => "1", "page" => "1",
# page size # page size
"offset" => "2" "offset" => "2",
"filterby" => "to"
} }
optional_params = AddressController.optional_params(params) 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.order_by_direction == :asc
assert optional_params.start_block == 100 assert optional_params.start_block == 100
assert optional_params.end_block == 120 assert optional_params.end_block == 120
assert optional_params.filter_by == "to"
end end
test "'sort' values can be 'asc' or 'desc'" do test "'sort' values can be 'asc' or 'desc'" do
@ -1552,6 +1554,18 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
assert AddressController.optional_params(params3) == %{} assert AddressController.optional_params(params3) == %{}
end 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 test "only includes optional params when they're given" do
assert AddressController.optional_params(%{}) == %{} assert AddressController.optional_params(%{}) == %{}
end end

@ -3,7 +3,7 @@ defmodule Explorer.Etherscan do
The etherscan context. 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.Etherscan.Logs
alias Explorer.{Repo, Chain} alias Explorer.{Repo, Chain}
@ -30,6 +30,10 @@ defmodule Explorer.Etherscan do
@doc """ @doc """
Gets a list of transactions for a given `t:Explorer.Chain.Hash.Address.t/0`. 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()] @spec list_transactions(Hash.Address.t()) :: [map()]
def list_transactions( def list_transactions(
@ -179,9 +183,6 @@ defmodule Explorer.Etherscan do
from( from(
t in Transaction, t in Transaction,
inner_join: b in assoc(t, :block), 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}], order_by: [{^options.order_by_direction, t.block_number}],
limit: ^options.page_size, limit: ^options.page_size,
offset: ^offset(options), offset: ^offset(options),
@ -193,11 +194,23 @@ defmodule Explorer.Etherscan do
) )
query query
|> where_address_match(address_hash, options)
|> where_start_block_match(options) |> where_start_block_match(options)
|> where_end_block_match(options) |> where_end_block_match(options)
|> Repo.all() |> Repo.all()
end 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_transfer_fields ~w(
token_contract_address_hash token_contract_address_hash
transaction_hash transaction_hash

@ -343,6 +343,42 @@ defmodule Explorer.EtherscanTest do
assert transaction.block_number in expected_block_numbers assert transaction.block_number in expected_block_numbers
end end
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 end
describe "list_internal_transactions/1" do describe "list_internal_transactions/1" do

Loading…
Cancel
Save