diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index d57a03ac69..5118f60950 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -91,13 +91,17 @@ defmodule Explorer.Chain do * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is `:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association, then the `t:Explorer.Chain.InternalTransaction.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, transaction_index, index}) and. Results will be the internal + transactions older than the block number, transaction index, and index that are passed. """ + @spec address_to_internal_transactions(Address.t(), [paging_options | necessity_by_association_option]) :: + InternalTransaction.t() def address_to_internal_transactions(%Address{hash: hash}, options \\ []) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) direction = Keyword.get(options, :direction) - pagination = Keyword.get(options, :pagination, %{}) + paging_options = Keyword.get(options, :paging_options, %PagingOptions{page_size: 50}) InternalTransaction |> join( @@ -108,6 +112,8 @@ defmodule Explorer.Chain do |> join(:left, [internal_transaction, transaction], block in assoc(transaction, :block)) |> where_address_fields_match(hash, direction) |> where_transaction_has_multiple_internal_transactions() + |> page_internal_transaction(paging_options) + |> limit(^paging_options.page_size) |> order_by( [it, transaction, block], desc: block.number, @@ -116,7 +122,7 @@ defmodule Explorer.Chain do ) |> preload(transaction: :block) |> join_associations(necessity_by_association) - |> Repo.paginate(pagination) + |> Repo.all() end @doc """ @@ -2525,6 +2531,19 @@ defmodule Explorer.Chain do ) end + defp page_internal_transaction(query, %PagingOptions{key: nil}), do: query + + defp page_internal_transaction(query, %PagingOptions{key: {block_number, transaction_index, index}}) do + query + |> where( + [internal_transaction, transaction], + transaction.block_number < ^block_number or + (transaction.block_number == ^block_number and transaction.index < ^transaction_index) or + (transaction.block_number == ^block_number and transaction.index == ^transaction_index and + internal_transaction.index < ^index) + ) + end + defp page_transaction(query, %PagingOptions{key: nil}), do: query defp page_transaction(query, %PagingOptions{key: {block_number, index}}) do diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index d19e8a41fb..43b9710b0b 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -405,7 +405,7 @@ defmodule Explorer.ChainTest do transaction: %Transaction{} } | _ - ] = Map.get(Chain.address_to_internal_transactions(address), :entries, []) + ] = Chain.address_to_internal_transactions(address) assert [ %InternalTransaction{ @@ -415,17 +415,13 @@ defmodule Explorer.ChainTest do } | _ ] = - Map.get( - Chain.address_to_internal_transactions( - address, - necessity_by_association: %{ - from_address: :optional, - to_address: :optional, - transaction: :optional - } - ), - :entries, - [] + Chain.address_to_internal_transactions( + address, + necessity_by_association: %{ + from_address: :optional, + to_address: :optional, + transaction: :optional + } ) end @@ -520,7 +516,6 @@ defmodule Explorer.ChainTest do result = address |> Chain.address_to_internal_transactions() - |> Map.get(:entries, []) |> Enum.map(& &1.id) assert [second_pending, first_pending, sixth, fifth, fourth, third, second, first] == result diff --git a/apps/explorer_web/lib/explorer_web/controllers/address_internal_transaction_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/address_internal_transaction_controller.ex index bf87e4d486..29de532635 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/address_internal_transaction_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/address_internal_transaction_controller.ex @@ -7,33 +7,49 @@ defmodule ExplorerWeb.AddressInternalTransactionController do import ExplorerWeb.AddressController, only: [transaction_count: 1] - alias Explorer.{Chain, Market} + alias Explorer.{Chain, Market, PagingOptions} 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, "transaction_index" => transaction_index_string, "index" => index_string} = params) do + with {block_number, ""} <- Integer.parse(block_number_string), + {transaction_index, ""} <- Integer.parse(transaction_index_string), + {index, ""} <- Integer.parse(index_string) do + do_index(conn, Map.put(params, :paging_options, %{@default_paging_options | key: {block_number, transaction_index, 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), {:ok, address} <- Chain.hash_to_address(address_hash) do - options = [ - necessity_by_association: %{ - from_address: :optional, - to_address: :optional - }, - pagination: params - ] - - page = - Chain.address_to_internal_transactions( - address, - Keyword.merge(options, current_filter(params)) + full_options = + Keyword.merge( + [ + necessity_by_association: %{ + from_address: :optional, + to_address: :optional + }, + paging_options: @default_paging_options + ], + current_filter(params) ) + internal_transactions = Chain.address_to_internal_transactions(address, full_options) + render( conn, "index.html", address: address, + earliest: earliest(internal_transactions), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], - page: page, + internal_transactions: internal_transactions, transaction_count: transaction_count(address) ) else @@ -45,6 +61,24 @@ defmodule ExplorerWeb.AddressInternalTransactionController do end end + defp earliest([]), do: nil + + defp earliest(internal_transactions) do + last = List.last(internal_transactions) + {:ok, last_transaction} = Chain.hash_to_transaction(last.transaction_hash) + %{block_number: last_transaction.block_number, transaction_index: last_transaction.index, 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 params |> Map.get("filter") diff --git a/apps/explorer_web/lib/explorer_web/templates/address_internal_transaction/index.html.eex b/apps/explorer_web/lib/explorer_web/templates/address_internal_transaction/index.html.eex index baf46761b7..242773d0e3 100644 --- a/apps/explorer_web/lib/explorer_web/templates/address_internal_transaction/index.html.eex +++ b/apps/explorer_web/lib/explorer_web/templates/address_internal_transaction/index.html.eex @@ -38,7 +38,7 @@
- <%= if Enum.count(@page) > 0 do %> + <%= if Enum.count(@internal_transactions) > 0 do %>
- - <%= if Enum.count(@page) > 0 do %> -
- <%= pagination_links( + <%= if @earliest do %> + <%= link( + gettext("Older"), + class: "button button--secondary button--sm u-float-right mt-3", + to: address_internal_transaction_path( @conn, - @page, - ["en", @conn.params["address_id"]], - distance: 1, - filter: @conn.params["filter"], - first: true, - next: Phoenix.HTML.raw("›"), - path: &address_internal_transaction_path/5, - previous: Phoenix.HTML.raw("‹"), - view_style: :bulma - ) %> -
+ :index, + @conn.assigns.locale, + @address, + %{"block_number" => @earliest.block_number, "transaction_index" => @earliest.transaction_index, "index" => @earliest.index} + ) + ) %> <% end %> diff --git a/apps/explorer_web/test/explorer_web/controllers/address_internal_transaction_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/address_internal_transaction_controller_test.exs index bd66d270a3..8ff8c5ec28 100644 --- a/apps/explorer_web/test/explorer_web/controllers/address_internal_transaction_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/address_internal_transaction_controller_test.exs @@ -3,6 +3,7 @@ defmodule ExplorerWeb.AddressInternalTransactionControllerTest do import ExplorerWeb.Router.Helpers, only: [address_internal_transaction_path: 4] + alias Explorer.Chain.InternalTransaction alias Explorer.ExchangeRates.Token describe "GET index/3" do @@ -38,7 +39,7 @@ defmodule ExplorerWeb.AddressInternalTransactionControllerTest do conn = get(conn, path) actual_transaction_ids = - conn.assigns.page + conn.assigns.internal_transactions |> Enum.map(fn internal_transaction -> internal_transaction.id end) assert Enum.member?(actual_transaction_ids, from_internal_transaction.id) @@ -52,5 +53,62 @@ defmodule ExplorerWeb.AddressInternalTransactionControllerTest do assert %Token{} = conn.assigns.exchange_rate end + + test "returns next page of results based on last seen internal transaction", %{conn: conn} do + address = insert(:address) + + a_block = insert(:block, number: 1000) + b_block = insert(:block, number: 2000) + + transaction_1 = + :transaction + |> insert() + |> with_block(a_block) + + transaction_2 = + :transaction + |> insert() + |> with_block(a_block) + + transaction_3 = + :transaction + |> insert() + |> with_block(b_block) + + transaction_1_hashes = + 1..20 + |> Enum.map(fn index -> insert(:internal_transaction, transaction_hash: transaction_1.hash, from_address_hash: address.hash, index: index) end) + |> Enum.map(& "#{&1.transaction_hash}.#{&1.index}") + + transaction_2_hashes = + 1..20 + |> Enum.map(fn index -> insert(:internal_transaction, transaction_hash: transaction_2.hash, from_address_hash: address.hash, index: index) end) + |> Enum.map(& "#{&1.transaction_hash}.#{&1.index}") + + transaction_3_hashes = + 1..10 + |> Enum.map(fn index -> insert(:internal_transaction, transaction_hash: transaction_3.hash, from_address_hash: address.hash, index: index) end) + |> Enum.map(& "#{&1.transaction_hash}.#{&1.index}") + + second_page_hashes = transaction_1_hashes ++ transaction_2_hashes ++ transaction_3_hashes + + %InternalTransaction{index: index} = + :internal_transaction + |> insert(transaction_hash: transaction_3.hash, from_address_hash: address.hash, index: 11) + + conn = + get(conn, address_internal_transaction_path(ExplorerWeb.Endpoint, :index, :en, address.hash), %{ + "block_number" => Integer.to_string(b_block.number), + "transaction_index" => Integer.to_string(transaction_3.index), + "index" => Integer.to_string(index) + }) + + actual_hashes = + conn.assigns.internal_transactions + |> Enum.map(& "#{&1.transaction_hash}.#{&1.index}") + |> Enum.reverse() + + assert second_page_hashes == actual_hashes + end end end