diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex new file mode 100644 index 0000000000..dcfde7452e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -0,0 +1,58 @@ +defmodule BlockScoutWeb.API.V2.InternalTransactionController do + use BlockScoutWeb, :controller + alias Explorer.Chain.InternalTransaction + alias Explorer.{Helper, PagingOptions} + + import BlockScoutWeb.Chain, + only: [ + split_list_by_page: 1, + paging_options: 1, + next_page_params: 3 + ] + + import BlockScoutWeb.PagingHelper, + only: [ + delete_parameters_from_next_page_params: 1 + ] + + import Explorer.PagingOptions, only: [default_paging_options: 0] + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @api_true [api?: true] + + @doc """ + Function to handle GET requests to `/api/v2/internal-transactions` endpoint. + """ + @spec internal_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() + def internal_transactions(conn, params) do + paging_options = paging_options(params) + + options = + paging_options + |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ + page_size: page_size + } = paging_options -> + maybe_parsed_limit = Helper.parse_integer(params["limit"]) + %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} + end) + |> Keyword.merge(@api_true) + + result = + options + |> InternalTransaction.fetch() + |> split_list_by_page() + + {internal_transactions, next_page} = result + + next_page_params = + next_page |> next_page_params(internal_transactions, delete_parameters_from_next_page_params(params)) + + conn + |> put_status(200) + |> render(:internal_transactions, %{ + internal_transactions: internal_transactions, + next_page_params: next_page_params + }) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index fe9bc6777e..506db2ab9f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -297,8 +297,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> - mb_parsed_limit = Helper.parse_integer(params["limit"]) - %PagingOptions{paging_options | page_size: min(page_size, mb_parsed_limit && abs(mb_parsed_limit))} + maybe_parsed_limit = Helper.parse_integer(params["limit"]) + %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} end) |> Keyword.merge(token_transfers_types_options(params)) |> Keyword.merge(tokens_sorting(params)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index f353bcc8bc..a917e8008b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -36,8 +36,8 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> - mb_parsed_limit = Helper.parse_integer(params["limit"]) - %PagingOptions{paging_options | page_size: min(page_size, mb_parsed_limit && abs(mb_parsed_limit))} + maybe_parsed_limit = Helper.parse_integer(params["limit"]) + %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} end) |> Keyword.merge(token_transfers_types_options(params)) |> Keyword.merge(@api_true) diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index f2daacb0c4..263559cb23 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do Module to interact with Transaction Interpretation Service """ - alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView, TransactionView} + alias BlockScoutWeb.API.V2.{Helper, InternalTransactionView, TokenTransferView, TokenView, TransactionView} alias Ecto.Association.NotLoaded alias Explorer.Chain alias Explorer.Chain.{Data, InternalTransaction, Log, TokenTransfer, Transaction} @@ -203,7 +203,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do defp prepare_internal_transactions(internal_transactions, transaction) do internal_transactions - |> Enum.map(&TransactionView.prepare_internal_transaction(&1, transaction.block)) + |> Enum.map(&InternalTransactionView.prepare_internal_transaction(&1, transaction.block)) end defp fetch_logs(transaction) do diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 42ec12c12d..e56e8c002e 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -168,6 +168,10 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/", V2.TokenTransferController, :token_transfers) end + scope "/internal-transactions" do + get("/", V2.InternalTransactionController, :internal_transactions) + end + scope "/blocks" do get("/", V2.BlockController, :blocks) get("/:block_hash_or_number", V2.BlockController, :block) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex new file mode 100644 index 0000000000..6ac34a0e0b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex @@ -0,0 +1,60 @@ +defmodule BlockScoutWeb.API.V2.InternalTransactionView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.V2.Helper + alias Explorer.Chain.{Block, InternalTransaction} + + def render("internal_transaction.json", %{internal_transaction: nil}) do + nil + end + + def render("internal_transaction.json", %{ + internal_transaction: internal_transaction, + block: block + }) do + prepare_internal_transaction(internal_transaction, block) + end + + def render("internal_transactions.json", %{ + internal_transactions: internal_transactions, + next_page_params: next_page_params + }) do + %{ + "items" => Enum.map(internal_transactions, &prepare_internal_transaction(&1, &1.block)), + "next_page_params" => next_page_params + } + end + + @doc """ + Prepares internal transaction object to be returned in the API v2 endpoints. + """ + @spec prepare_internal_transaction(InternalTransaction.t(), Block.t() | nil) :: map() + def prepare_internal_transaction(internal_transaction, block \\ nil) do + %{ + "error" => internal_transaction.error, + "success" => is_nil(internal_transaction.error), + "type" => internal_transaction.call_type || internal_transaction.type, + "transaction_hash" => internal_transaction.transaction_hash, + "transaction_index" => internal_transaction.transaction_index, + "from" => + Helper.address_with_info(nil, internal_transaction.from_address, internal_transaction.from_address_hash, false), + "to" => + Helper.address_with_info(nil, internal_transaction.to_address, internal_transaction.to_address_hash, false), + "created_contract" => + Helper.address_with_info( + nil, + internal_transaction.created_contract_address, + internal_transaction.created_contract_address_hash, + false + ), + "value" => internal_transaction.value, + # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `block_number` property + "block" => internal_transaction.block_number, + "block_number" => internal_transaction.block_number, + "timestamp" => (block && block.timestamp) || internal_transaction.block.timestamp, + "index" => internal_transaction.index, + "gas_limit" => internal_transaction.gas, + "block_index" => internal_transaction.block_index + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex index 33c52bd9bd..6d4ec1d001 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do alias BlockScoutWeb.Tokens.Helper, as: TokensHelper alias Ecto.Association.NotLoaded alias Explorer.Chain - alias Explorer.Chain.Transaction + alias Explorer.Chain.{TokenTransfer, Transaction} def render("token_transfer.json", %{token_transfer: nil}) do nil @@ -39,6 +39,10 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do } end + @doc """ + Prepares token transfer object to be returned in the API v2 endpoints. + """ + @spec prepare_token_transfer(TokenTransfer.t(), Plug.Conn.t() | nil, any()) :: map() def prepare_token_transfer(token_transfer, _conn, decoded_input) do %{ "transaction_hash" => token_transfer.transaction_hash, @@ -61,6 +65,10 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do } end + @doc """ + Prepares token transfer total value/id transferred to be returned in the API v2 endpoints. + """ + @spec prepare_token_transfer_total(TokenTransfer.t()) :: map() # credo:disable-for-next-line /Complexity/ def prepare_token_transfer_total(token_transfer) do case TokensHelper.token_transfer_amount_for_api(token_transfer) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index b569c7d100..36511e45be 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do use BlockScoutWeb, :view - alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenTransferView, TokenView} + alias BlockScoutWeb.API.V2.{ApiView, Helper, InternalTransactionView, TokenTransferView, TokenView} alias BlockScoutWeb.{ABIEncodedValueView, TransactionView} alias BlockScoutWeb.Models.GetTransactionTags @@ -155,7 +155,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do block: block }) do %{ - "items" => Enum.map(internal_transactions, &prepare_internal_transaction(&1, block)), + "items" => Enum.map(internal_transactions, &InternalTransactionView.prepare_internal_transaction(&1, block)), "next_page_params" => next_page_params } end @@ -165,7 +165,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do next_page_params: next_page_params }) do %{ - "items" => Enum.map(internal_transactions, &prepare_internal_transaction(&1)), + "items" => Enum.map(internal_transactions, &InternalTransactionView.prepare_internal_transaction(&1)), "next_page_params" => next_page_params } end @@ -252,34 +252,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end - def prepare_internal_transaction(internal_transaction, block \\ nil) do - %{ - "error" => internal_transaction.error, - "success" => is_nil(internal_transaction.error), - "type" => internal_transaction.call_type || internal_transaction.type, - "transaction_hash" => internal_transaction.transaction_hash, - "from" => - Helper.address_with_info(nil, internal_transaction.from_address, internal_transaction.from_address_hash, false), - "to" => - Helper.address_with_info(nil, internal_transaction.to_address, internal_transaction.to_address_hash, false), - "created_contract" => - Helper.address_with_info( - nil, - internal_transaction.created_contract_address, - internal_transaction.created_contract_address_hash, - false - ), - "value" => internal_transaction.value, - "block_number" => internal_transaction.block_number, - # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `block_number` property - "block" => internal_transaction.block_number, - "timestamp" => (block && block.timestamp) || internal_transaction.block.timestamp, - "index" => internal_transaction.index, - "gas_limit" => internal_transaction.gas, - "block_index" => internal_transaction.block_index - } - end - def prepare_log(log, transaction_or_hash, decoded_log, tags_for_address_needed? \\ false) do decoded = process_decoded_log(decoded_log) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs new file mode 100644 index 0000000000..2f9dad6558 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs @@ -0,0 +1,93 @@ +defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, InternalTransaction} + + describe "/internal-transactions" do + test "empty list", %{conn: conn} do + request = get(conn, "/api/v2/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "non empty list", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction: tx, + block_hash: tx.block_hash, + index: 0, + block_index: 0 + ) + + request = get(conn, "/api/v2/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + end + + test "internal transactions with next_page_params", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + transaction_index: 0, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + index: 0, + block_index: 0 + ) + + transaction_2 = insert(:transaction) |> with_block() + + internal_transactions = + for i <- 0..49 do + insert(:internal_transaction, + transaction: transaction_2, + transaction_index: 0, + block_number: transaction_2.block_number, + block_hash: transaction_2.block_hash, + index: i, + block_index: i + ) + end + + internal_transactions = [internal_transaction | internal_transactions] + + request = get(conn, "/api/v2/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/internal-transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions) + end + end + + defp compare_item(%InternalTransaction{} = internal_transaction, json) do + assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] + assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] + assert internal_transaction.block_number == json["block_number"] + assert internal_transaction.block_index == json["block_index"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, internal_transactions) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(internal_transactions, 50), Enum.at(first_page_resp["items"], 0)) + + compare_item(Enum.at(internal_transactions, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(internal_transactions, 0), Enum.at(second_page_resp["items"], 0)) + end +end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 2e4f3525b1..27db2c00e1 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3411,28 +3411,28 @@ defmodule Explorer.Chain do # todo: keep next clause for compatibility with frontend and remove when new frontend is bound to `index_internal_transaction_desc_order` property def page_internal_transaction(query, %PagingOptions{key: {block_number, transaction_index, index}}, %{ - index_int_tx_desc_order: desc + index_int_tx_desc_order: desc_order }) do - hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, desc) + hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, desc_order) end def page_internal_transaction(query, %PagingOptions{key: {block_number, transaction_index, index}}, %{ - index_internal_transaction_desc_order: desc + index_internal_transaction_desc_order: desc_order }) do - hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, desc) + hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, desc_order) end # todo: keep next clause for compatibility with frontend and remove when new frontend is bound to `index_internal_transaction_desc_order` property - def page_internal_transaction(query, %PagingOptions{key: {0}}, %{index_int_tx_desc_order: desc}) do - if desc do + def page_internal_transaction(query, %PagingOptions{key: {0}}, %{index_int_tx_desc_order: desc_order}) do + if desc_order do query else where(query, [internal_transaction], internal_transaction.index > 0) end end - def page_internal_transaction(query, %PagingOptions{key: {0}}, %{index_internal_transaction_desc_order: desc}) do - if desc do + def page_internal_transaction(query, %PagingOptions{key: {0}}, %{index_internal_transaction_desc_order: desc_order}) do + if desc_order do query else where(query, [internal_transaction], internal_transaction.index > 0) @@ -3440,16 +3440,18 @@ defmodule Explorer.Chain do end # todo: keep next clause for compatibility with frontend and remove when new frontend is bound to `index_internal_transaction_desc_order` property - def page_internal_transaction(query, %PagingOptions{key: {index}}, %{index_int_tx_desc_order: desc}) do - if desc do + def page_internal_transaction(query, %PagingOptions{key: {index}}, %{index_int_tx_desc_order: desc_order}) do + if desc_order do where(query, [internal_transaction], internal_transaction.index < ^index) else where(query, [internal_transaction], internal_transaction.index > ^index) end end - def page_internal_transaction(query, %PagingOptions{key: {index}}, %{index_internal_transaction_desc_order: desc}) do - if desc do + def page_internal_transaction(query, %PagingOptions{key: {index}}, %{ + index_internal_transaction_desc_order: desc_order + }) do + if desc_order do where(query, [internal_transaction], internal_transaction.index < ^index) else where(query, [internal_transaction], internal_transaction.index > ^index) diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 2661c63e4c..3d62885d34 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -5,8 +5,12 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} + alias Explorer.Chain.DenormalizationHelper alias Explorer.Chain.InternalTransaction.{Action, CallType, Result, Type} + @typep paging_options :: {:paging_options, PagingOptions.t()} + @typep api? :: {:api?, true | false} + @default_paging_options %PagingOptions{page_size: 50} @typedoc """ @@ -813,6 +817,39 @@ defmodule Explorer.Chain.InternalTransaction do ) end + @doc """ + Returns the ordered paginated list of internal transactions (consensus blocks only) from the DB with address, block preloads + """ + @spec fetch([paging_options | api?]) :: [] + def fetch(options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + case paging_options do + %PagingOptions{key: {0, 0}} -> + [] + + _ -> + preloads = + DenormalizationHelper.extend_transaction_preload([ + :block, + [from_address: [:names, :smart_contract, :proxy_implementations]], + [to_address: [:names, :smart_contract, :proxy_implementations]] + ]) + + __MODULE__ + |> where_nonpending_block() + |> Chain.page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) + |> order_by([internal_transaction], + desc: internal_transaction.block_number, + desc: internal_transaction.transaction_index, + desc: internal_transaction.index + ) + |> limit(^paging_options.page_size) + |> preload(^preloads) + |> Chain.select_repo(options).all() + end + end + defp page_block_internal_transaction(query, %PagingOptions{key: %{block_index: block_index}}) do query |> where([internal_transaction], internal_transaction.block_index > ^block_index) diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 9ace1518d4..1b9e3ac0ac 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -282,6 +282,9 @@ defmodule Explorer.Chain.TokenTransfer do end end + @doc """ + Returns the ordered paginated list of consensus token transfers (consensus blocks only) from the DB with address, token, transaction preloads + """ @spec fetch([paging_options | api?]) :: [] def fetch(options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options)