Change to last seen paging method for address transactions

pull/306/head
Stamates 7 years ago
parent fa5f60a364
commit c807fa539a
  1. 35
      apps/explorer/lib/explorer/chain.ex
  2. 110
      apps/explorer/test/explorer/chain_test.exs
  3. 66
      apps/explorer_web/lib/explorer_web/controllers/address_transaction_controller.ex
  4. 30
      apps/explorer_web/lib/explorer_web/templates/address_transaction/index.html.eex
  5. 39
      apps/explorer_web/test/explorer_web/controllers/address_transaction_controller_test.exs

@ -60,7 +60,6 @@ defmodule Explorer.Chain do
""" """
@type pagination :: map() @type pagination :: map()
@typep direction_option :: {:direction, direction}
@typep inserted_after_option :: {:inserted_after, DateTime.t()} @typep inserted_after_option :: {:inserted_after, DateTime.t()}
@typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association} @typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association}
@typep on_conflict_option :: {:on_conflict, :nothing | :replace_all} @typep on_conflict_option :: {:on_conflict, :nothing | :replace_all}
@ -135,19 +134,15 @@ defmodule Explorer.Chain do
## Options ## Options
* `:direction` - if specified, will filter transactions by address type. If `:to` is specified, only transactions
where the "to" address matches will be returned. Likewise, if `:from` is specified, only transactions where the
"from" address matches will be returned. If `:direction` is omitted, transactions either to or from the address
will be returned.
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. `t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`.
* `:pagination` - pagination params to pass to scrivener. * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (a tuple of the lowest/oldest {block_number, index}) and. Results will be the transactions older than
the block number and index that are passed.
""" """
@spec address_to_transactions(Address.t(), [ @spec address_to_transactions(Address.t(), [paging_options | necessity_by_association_option]) :: Transaction.t()
direction_option | necessity_by_association_option | pagination_option
]) :: %Scrivener.Page{entries: [Transaction.t()]}
def address_to_transactions(%Address{hash: hash}, options \\ []) when is_list(options) do def address_to_transactions(%Address{hash: hash}, options \\ []) when is_list(options) do
address_hash_to_transactions(hash, options) address_hash_to_transactions(hash, options)
end end
@ -2197,22 +2192,24 @@ defmodule Explorer.Chain do
defp address_hash_to_transactions( defp address_hash_to_transactions(
%Hash{byte_count: unquote(Hash.Truncated.byte_count())} = address_hash, %Hash{byte_count: unquote(Hash.Truncated.byte_count())} = address_hash,
named_arguments options
) )
when is_list(named_arguments) do when is_list(options) do
direction = Keyword.get(named_arguments, :direction) direction = Keyword.get(options, :direction)
necessity_by_association = Keyword.get(named_arguments, :necessity_by_association, %{}) necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
pagination = Keyword.get(named_arguments, :pagination, %{}) paging_options = Keyword.get(options, :paging_options, %PagingOptions{page_size: 50})
Transaction Transaction
|> load_contract_creation() |> load_contract_creation()
|> select_merge([_, internal_transaction], %{ |> select_merge([_, internal_transaction], %{
created_contract_address_hash: internal_transaction.created_contract_address_hash created_contract_address_hash: internal_transaction.created_contract_address_hash
}) })
|> join_associations(necessity_by_association)
|> reverse_chronologically()
|> where_address_fields_match(address_hash, direction) |> where_address_fields_match(address_hash, direction)
|> Repo.paginate(pagination) |> page_transaction(paging_options)
|> limit(^paging_options.page_size)
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
|> join_associations(necessity_by_association)
|> Repo.all()
end end
@spec changes_list(params :: map, [{:for, module} | {:with, :atom}]) :: @spec changes_list(params :: map, [{:for, module} | {:with, :atom}]) ::
@ -2539,10 +2536,6 @@ defmodule Explorer.Chain do
) )
end end
defp reverse_chronologically(query) do
from(q in query, order_by: [desc: q.inserted_at, desc: q.hash])
end
defp run_addresses(multi, ecto_schema_module_to_changes_list, options) defp run_addresses(multi, ecto_schema_module_to_changes_list, options)
when is_map(ecto_schema_module_to_changes_list) and is_list(options) do when is_map(ecto_schema_module_to_changes_list) and is_list(options) do
case ecto_schema_module_to_changes_list do case ecto_schema_module_to_changes_list do

@ -3,8 +3,8 @@ defmodule Explorer.ChainTest do
import Explorer.Factory import Explorer.Factory
alias Explorer.{Chain, Repo, Factory} alias Explorer.{Chain, Factory, PagingOptions, Repo}
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Transaction, Wei, SmartContract} alias Explorer.Chain.{Address, Block, InternalTransaction, Log, SmartContract, Transaction, Wei}
alias Explorer.Chain.Supply.ProofOfAuthority alias Explorer.Chain.Supply.ProofOfAuthority
doctest Explorer.Chain doctest Explorer.Chain
@ -30,106 +30,70 @@ defmodule Explorer.ChainTest do
assert Repo.aggregate(Transaction, :count, :hash) == 0 assert Repo.aggregate(Transaction, :count, :hash) == 0
assert %Scrivener.Page{ assert [] == Chain.address_to_transactions(address)
entries: [],
page_number: 1,
total_entries: 0
} = Chain.address_to_transactions(address)
end end
test "with from transactions" do test "with from transactions" do
%Transaction{from_address_hash: from_address_hash, hash: transaction_hash} = insert(:transaction) address = insert(:address)
address = Repo.get!(Address, from_address_hash) transaction = insert(:transaction, from_address_hash: address.hash)
assert %Scrivener.Page{ assert [transaction] == Chain.address_to_transactions(address, direction: :from)
entries: [%Transaction{hash: ^transaction_hash}],
page_number: 1,
total_entries: 1
} = Chain.address_to_transactions(address, direction: :from)
end end
test "with to transactions" do test "with to transactions" do
%Transaction{to_address_hash: to_address_hash, hash: transaction_hash} = insert(:transaction) address = insert(:address)
address = Repo.get!(Address, to_address_hash) transaction = insert(:transaction, to_address_hash: address.hash)
assert %Scrivener.Page{ assert [transaction] == Chain.address_to_transactions(address, direction: :to)
entries: [%Transaction{hash: ^transaction_hash}],
page_number: 1,
total_entries: 1
} = Chain.address_to_transactions(address, direction: :to)
end end
test "with to and from transactions and direction: :from" do test "with to and from transactions and direction: :from" do
%Transaction{from_address: address, hash: from_transaction_hash} = address = insert(:address)
:transaction transaction = insert(:transaction, from_address_hash: address.hash)
|> insert() insert(:transaction, to_address_hash: address.hash)
|> Repo.preload(:from_address)
insert(:transaction, to_address: address)
# only contains "from" transaction # only contains "from" transaction
assert %Scrivener.Page{ assert [transaction] == Chain.address_to_transactions(address, direction: :from)
entries: [%Transaction{hash: ^from_transaction_hash}],
page_number: 1,
total_entries: 1
} = Chain.address_to_transactions(address, direction: :from)
end end
test "with to and from transactions and direction: :to" do test "with to and from transactions and direction: :to" do
%Transaction{from_address: address} = address = insert(:address)
:transaction transaction = insert(:transaction, to_address_hash: address.hash)
|> insert() insert(:transaction, from_address_hash: address.hash)
|> Repo.preload(:from_address)
%Transaction{hash: to_transaction_hash} = insert(:transaction, to_address: address)
# only contains "to" transaction # only contains "to" transaction
assert %Scrivener.Page{ assert [transaction] == Chain.address_to_transactions(address, direction: :to)
entries: [%Transaction{hash: ^to_transaction_hash}],
page_number: 1,
total_entries: 1
} = Chain.address_to_transactions(address, direction: :to)
end end
test "with to and from transactions and no :direction option" do test "with to and from transactions and no :direction option" do
%Transaction{from_address: address, hash: from_transaction_hash} = address = insert(:address)
:transaction transaction1 = insert(:transaction, to_address_hash: address.hash)
|> insert() transaction2 = insert(:transaction, from_address_hash: address.hash)
|> Repo.preload(:from_address)
%Transaction{hash: to_transaction_hash} = insert(:transaction, to_address: address)
assert %Scrivener.Page{ assert [transaction1, transaction2] == Chain.address_to_transactions(address)
entries: [
%Transaction{hash: ^to_transaction_hash},
%Transaction{hash: ^from_transaction_hash}
],
page_number: 1,
total_entries: 2
} = Chain.address_to_transactions(address)
end end
test "with transactions can be paginated" do test "with transactions can be paginated" do
address = insert(:address) address = insert(:address)
transactions = insert_list(2, :transaction, to_address: address)
[%Transaction{hash: oldest_transaction_hash}, %Transaction{hash: newest_transaction_hash}] = transactions second_page_hashes =
50
|> insert_list(:transaction, from_address_hash: address.hash)
|> with_block()
|> Enum.map(& &1.hash)
assert %Scrivener.Page{ %Transaction{block_number: block_number, index: index} =
entries: [%Transaction{hash: ^newest_transaction_hash}], :transaction
page_number: 1, |> insert(from_address_hash: address.hash)
page_size: 1, |> with_block()
total_entries: 2,
total_pages: 2
} = Chain.address_to_transactions(address, pagination: %{page_size: 1})
assert %Scrivener.Page{ assert second_page_hashes ==
entries: [%Transaction{hash: ^oldest_transaction_hash}], address
page_number: 2, |> Chain.address_to_transactions(
page_size: 1, paging_options: %PagingOptions{key: {block_number, index}, page_size: 50}
total_entries: 2, )
total_pages: 2 |> Enum.map(& &1.hash)
} = Chain.address_to_transactions(address, pagination: %{page: 2, page_size: 1}) |> Enum.reverse()
end end
end end

@ -7,45 +7,77 @@ defmodule ExplorerWeb.AddressTransactionController do
import ExplorerWeb.AddressController, only: [transaction_count: 1] import ExplorerWeb.AddressController, only: [transaction_count: 1]
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market, PagingOptions}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
def index(conn, %{"address_id" => address_hash_string} = params) do @default_paging_options %PagingOptions{page_size: 50}
def index(conn, %{"block_number" => block_number_string, "index" => index_string} = params) do
with {block_number, ""} <- Integer.parse(block_number_string),
{index, ""} <- Integer.parse(index_string) do
do_index(conn, Map.put(params, :paging_options, %{@default_paging_options | key: {block_number, index}}))
else
_ ->
unprocessable_entity(conn)
end
end
def index(conn, params), do: do_index(conn, params)
def do_index(conn, %{"address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do {:ok, address} <- Chain.hash_to_address(address_hash) do
options = [ full_options =
necessity_by_association: %{ Keyword.merge(
block: :required, [
from_address: :optional, necessity_by_association: %{
to_address: :optional block: :required,
}, from_address: :optional,
pagination: params to_address: :optional
] },
paging_options: @default_paging_options
page = ],
Chain.address_to_transactions( current_filter(params)
address,
Keyword.merge(options, current_filter(params))
) )
transactions = Chain.address_to_transactions(address, full_options)
render( render(
conn, conn,
"index.html", "index.html",
address: address, address: address,
earliest: earliest(transactions),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"], filter: params["filter"],
page: page, transactions: transactions,
transaction_count: transaction_count(address) transaction_count: transaction_count(address)
) )
else else
:error -> :error ->
not_found(conn) unprocessable_entity(conn)
{:error, :not_found} -> {:error, :not_found} ->
not_found(conn) not_found(conn)
end end
end end
defp earliest([]), do: nil
defp earliest(transactions) do
last = List.last(transactions)
%{block_number: last.block_number, index: last.index}
end
defp current_filter(%{paging_options: paging_options} = params) do
params
|> Map.get("filter")
|> case do
"to" -> [direction: :to, paging_options: paging_options]
"from" -> [direction: :from, paging_options: paging_options]
_ -> [paging_options: paging_options]
end
end
defp current_filter(params) do defp current_filter(params) do
params params
|> Map.get("filter") |> Map.get("filter")

@ -93,7 +93,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<%= for transaction <- @page do %> <%= for transaction <- @transactions do %>
<tr> <tr>
<td><div class="transaction__dot transaction__dot--<%= status(transaction) %>"></div></td> <td><div class="transaction__dot transaction__dot--<%= status(transaction) %>"></div></td>
<td> <td>
@ -134,20 +134,18 @@
</table> </table>
</div> </div>
</div> </div>
<%= if @earliest do %>
<div class="address__pagination"> <%= link(
<%= pagination_links( gettext("Older"),
@conn, class: "button button--secondary button--sm u-float-right mt-3",
@page, to: address_transaction_path(
["en", @conn.params["address_id"]], @conn,
distance: 1, :index,
filter: @conn.params["filter"], @conn.assigns.locale,
first: true, @address,
next: Phoenix.HTML.raw("&rsaquo;"), %{"block_number" => @earliest.block_number, "index" => @earliest.index}
path: &address_transaction_path/5, )
previous: Phoenix.HTML.raw("&lsaquo;"), ) %>
view_style: :bulma <% end %>
) %>
</div>
</section> </section>
</section> </section>

@ -3,13 +3,14 @@ defmodule ExplorerWeb.AddressTransactionControllerTest do
import ExplorerWeb.Router.Helpers, only: [address_transaction_path: 4] import ExplorerWeb.Router.Helpers, only: [address_transaction_path: 4]
alias Explorer.Chain.Transaction
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
describe "GET index/2" do describe "GET index/2" do
test "with invalid address hash", %{conn: conn} do test "with invalid address hash", %{conn: conn} do
conn = get(conn, address_transaction_path(conn, :index, :en, "invalid_address")) conn = get(conn, address_transaction_path(conn, :index, :en, "invalid_address"))
assert html_response(conn, 404) assert html_response(conn, 422)
end end
test "with valid address hash without address", %{conn: conn} do test "with valid address hash without address", %{conn: conn} do
@ -36,8 +37,8 @@ defmodule ExplorerWeb.AddressTransactionControllerTest do
conn = get(conn, address_transaction_path(conn, :index, :en, address)) conn = get(conn, address_transaction_path(conn, :index, :en, address))
actual_transaction_hashes = actual_transaction_hashes =
conn.assigns.page conn.assigns.transactions
|> Enum.map(fn transaction -> transaction.hash end) |> Enum.map(& &1.hash)
assert html_response(conn, 200) assert html_response(conn, 200)
assert Enum.member?(actual_transaction_hashes, from_transaction.hash) assert Enum.member?(actual_transaction_hashes, from_transaction.hash)
@ -53,9 +54,9 @@ defmodule ExplorerWeb.AddressTransactionControllerTest do
assert html_response(conn, 200) assert html_response(conn, 200)
assert conn.status == 200 assert conn.status == 200
assert Enum.empty?(conn.assigns.page) assert Enum.empty?(conn.assigns.transactions)
assert conn.status == 200 assert conn.status == 200
assert Enum.empty?(conn.assigns.page) assert Enum.empty?(conn.assigns.transactions)
end end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do test "includes USD exchange rate value for address in assigns", %{conn: conn} do
@ -65,5 +66,33 @@ defmodule ExplorerWeb.AddressTransactionControllerTest do
assert %Token{} = conn.assigns.exchange_rate assert %Token{} = conn.assigns.exchange_rate
end end
test "returns next page of results based on last seen transaction", %{conn: conn} do
address = insert(:address)
second_page_hashes =
50
|> insert_list(:transaction, from_address_hash: address.hash)
|> with_block()
|> Enum.map(& &1.hash)
%Transaction{block_number: block_number, index: index} =
:transaction
|> insert(from_address_hash: address.hash)
|> with_block()
conn =
get(conn, address_transaction_path(ExplorerWeb.Endpoint, :index, :en, address.hash), %{
"block_number" => Integer.to_string(block_number),
"index" => Integer.to_string(index)
})
actual_hashes =
conn.assigns.transactions
|> Enum.map(& &1.hash)
|> Enum.reverse()
assert second_page_hashes == actual_hashes
end
end end
end end

Loading…
Cancel
Save