feat: List of internal transactions API v2 endpoint (#10994)

* feat: List of internal transactions API v2 endpoint

* Process Fedor review

* Fix format

* Process review comment
pull/11008/head
Victor Baranov 1 month ago committed by GitHub
parent fc0c5b5315
commit aa3defae18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 58
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex
  2. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  3. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex
  4. 4
      apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
  5. 4
      apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
  6. 60
      apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex
  7. 10
      apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex
  8. 34
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  9. 93
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs
  10. 26
      apps/explorer/lib/explorer/chain.ex
  11. 37
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  12. 3
      apps/explorer/lib/explorer/chain/token_transfer.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

@ -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))

@ -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)

@ -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

@ -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)

@ -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

@ -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

@ -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)

@ -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

@ -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)

@ -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)

@ -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)

Loading…
Cancel
Save