GraphQL support to get internal transactions

Why:

* For GraphQL API users to be able to get internal transactions. RPC API
users could use this instead of the `txlistinternal` RPC API action.

  Example usage:

  1.
  ```
  query ($hash: FullHash!, $first: Int!) {
    transaction(hash: $hash) {
      internal_transactions(first: $first) {
        edges {
          node {
            index
            value
            block_number
            transaction_index
            from_address_hash
            to_address_hash
          }
        }
      }
    }
  }
  ```

  2. with pagiantion support via [Relay Cursor
     Connections](https://facebook.github.io/relay/graphql/connections.htm)
  ```
  query ($hash: AddressHash!, $first: Int!, $after: ID!) {
    transaction(hash: $hash) {
      internal_transactions(first: $first, after: $after) {
        page_info {
          has_next_page
          has_previous_page
        }
        edges {
          node {
            index
            transaction_hash
          }
          cursor
        }
      }
    }
  }
  ```
* Issue link: n/a

This change addresses the need by:

* Editing the node interface and node field in `BlockScoutWeb.Schema` to
support internal transactions.
* Adding `internal_transaction` object type to
`BlockScoutWeb.Schema.Types`.
* Adding an `internal_transaction` connection field to the `transaction`
object type in `BlockScoutWeb.Schema.Types`.
* Adding `:call_type` and `:type` enums to
`BlockScoutWEb.Schema.Scalars`. Needed by `internal_transaction` GraphQL
object type.
* Creating `BlockScoutWeb.Resolvers.InternalTransaction` with resolver
functions for getting internal transactions.
* Adding `get_internal_transactions/1` and
`transaction_to_internal_transactions_query/1` to `Explorer.GraphQL`.
These are used by the new resolver functions mentioned above.
pull/1153/head
Sebastian Abondano 6 years ago committed by Luke Imhoff
parent ed31a3c6a9
commit 24079bb741
  1. 2
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  2. 23
      apps/block_scout_web/lib/block_scout_web/resolvers/internal_transaction.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex
  4. 18
      apps/block_scout_web/lib/block_scout_web/schema.ex
  5. 14
      apps/block_scout_web/lib/block_scout_web/schema/scalars.ex
  6. 47
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  7. 22
      apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs
  8. 95
      apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs
  9. 394
      apps/block_scout_web/test/block_scout_web/schema/query/transaction_test.exs
  10. 50
      apps/explorer/lib/explorer/graphql.ex
  11. 94
      apps/explorer/test/explorer/graphql_test.exs

@ -1012,7 +1012,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_txlistinternal_action %{ @account_txlistinternal_action %{
name: "txlistinternal", name: "txlistinternal",
description: description:
"Get internal transactions by transaction or address hash. Up to a maximum of 10,000 internal transactions.", "Get internal transactions by transaction or address hash. Up to a maximum of 10,000 internal transactions. Also available through a GraphQL 'transaction' query.",
required_params: [ required_params: [
%{ %{
key: "txhash", key: "txhash",

@ -0,0 +1,23 @@
defmodule BlockScoutWeb.Resolvers.InternalTransaction do
@moduledoc false
alias Absinthe.Relay.Connection
alias Explorer.Chain.Transaction
alias Explorer.{GraphQL, Repo}
def get_by(%{transaction_hash: _, index: _} = args) do
GraphQL.get_internal_transaction(args)
end
def get_by(%Transaction{} = transaction, args, _) do
transaction
|> GraphQL.transaction_to_internal_transactions_query()
|> Connection.from_query(&Repo.all/1, args, options(args))
end
defp options(%{before: _}), do: []
defp options(%{count: count}), do: [count: count]
defp options(_), do: []
end

@ -8,7 +8,7 @@ defmodule BlockScoutWeb.Resolvers.Transaction do
def get_by(_, %{hash: hash}, _) do def get_by(_, %{hash: hash}, _) do
case Chain.hash_to_transaction(hash) do case Chain.hash_to_transaction(hash) do
{:ok, transaction} -> {:ok, transaction} {:ok, transaction} -> {:ok, transaction}
{:error, :not_found} -> {:error, "Transaction hash #{hash} was not found."} {:error, :not_found} -> {:error, "Transaction not found."}
end end
end end

@ -6,8 +6,16 @@ defmodule BlockScoutWeb.Schema do
alias Absinthe.Middleware.Dataloader, as: AbsintheMiddlewareDataloader alias Absinthe.Middleware.Dataloader, as: AbsintheMiddlewareDataloader
alias Absinthe.Plugin, as: AbsinthePlugin alias Absinthe.Plugin, as: AbsinthePlugin
alias BlockScoutWeb.Resolvers.{Address, Block, Transaction}
alias BlockScoutWeb.Resolvers.{
Address,
Block,
InternalTransaction,
Transaction
}
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.InternalTransaction, as: ExplorerChainInternalTransaction
alias Explorer.Chain.Transaction, as: ExplorerChainTransaction alias Explorer.Chain.Transaction, as: ExplorerChainTransaction
import_types(BlockScoutWeb.Schema.Types) import_types(BlockScoutWeb.Schema.Types)
@ -17,6 +25,9 @@ defmodule BlockScoutWeb.Schema do
%ExplorerChainTransaction{}, _ -> %ExplorerChainTransaction{}, _ ->
:transaction :transaction
%ExplorerChainInternalTransaction{}, _ ->
:internal_transaction
_, _ -> _, _ ->
nil nil
end) end)
@ -29,6 +40,11 @@ defmodule BlockScoutWeb.Schema do
{:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string) {:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string)
Transaction.get_by(%{}, %{hash: hash}, %{}) Transaction.get_by(%{}, %{hash: hash}, %{})
%{type: :internal_transaction, id: id}, _ ->
%{"transaction_hash" => transaction_hash_string, "index" => index} = Jason.decode!(id)
{:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string)
InternalTransaction.get_by(%{transaction_hash: transaction_hash, index: index})
_, _ -> _, _ ->
{:error, "Unknown node"} {:error, "Unknown node"}
end) end)

@ -99,4 +99,18 @@ defmodule BlockScoutWeb.Schema.Scalars do
value(:ok) value(:ok)
value(:error) value(:error)
end end
enum :call_type do
value(:call)
value(:callcode)
value(:delegatecall)
value(:staticcall)
end
enum :type do
value(:call)
value(:create)
value(:reward)
value(:selfdestruct)
end
end end

@ -6,12 +6,16 @@ defmodule BlockScoutWeb.Schema.Types do
import Absinthe.Resolution.Helpers import Absinthe.Resolution.Helpers
alias BlockScoutWeb.Resolvers.Transaction alias BlockScoutWeb.Resolvers.{
InternalTransaction,
Transaction
}
import_types(Absinthe.Type.Custom) import_types(Absinthe.Type.Custom)
import_types(BlockScoutWeb.Schema.Scalars) import_types(BlockScoutWeb.Schema.Scalars)
connection(node_type: :transaction) connection(node_type: :transaction)
connection(node_type: :internal_transaction)
@desc """ @desc """
A stored representation of a Web3 address. A stored representation of a Web3 address.
@ -60,6 +64,30 @@ defmodule BlockScoutWeb.Schema.Types do
field(:parent_hash, :full_hash) field(:parent_hash, :full_hash)
end end
@desc """
Models internal transactions.
"""
node object(:internal_transaction, id_fetcher: &internal_transaction_id_fetcher/2) do
field(:call_type, :call_type)
field(:created_contract_code, :data)
field(:error, :string)
field(:gas, :decimal)
field(:gas_used, :decimal)
field(:index, :integer)
field(:init, :data)
field(:input, :data)
field(:output, :data)
field(:trace_address, :json)
field(:type, :type)
field(:value, :wei)
field(:block_number, :integer)
field(:transaction_index, :integer)
field(:created_contract_address_hash, :address_hash)
field(:from_address_hash, :address_hash)
field(:to_address_hash, :address_hash)
field(:transaction_hash, :full_hash)
end
@desc """ @desc """
The representation of a verified Smart Contract. The representation of a verified Smart Contract.
@ -99,6 +127,19 @@ defmodule BlockScoutWeb.Schema.Types do
field(:from_address_hash, :address_hash) field(:from_address_hash, :address_hash)
field(:to_address_hash, :address_hash) field(:to_address_hash, :address_hash)
field(:created_contract_address_hash, :address_hash) field(:created_contract_address_hash, :address_hash)
connection field(:internal_transactions, node_type: :internal_transaction) do
arg(:count, :integer)
resolve(&InternalTransaction.get_by/3)
complexity(fn
%{first: first}, child_complexity ->
first * child_complexity
%{last: last}, child_complexity ->
last * child_complexity
end)
end
end end
@desc """ @desc """
@ -113,4 +154,8 @@ defmodule BlockScoutWeb.Schema.Types do
end end
def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash) def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash)
def internal_transaction_id_fetcher(%{transaction_hash: transaction_hash, index: index}, _) do
Jason.encode!(%{transaction_hash: to_string(transaction_hash), index: index})
end
end end

@ -515,33 +515,13 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
|> List.last() |> List.last()
|> Map.get("cursor") |> Map.get("cursor")
query3 = """
query ($hash: AddressHash!, $first: Int!, $after: Int!) {
address(hash: $hash) {
transactions(first: $first, after: $after) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
hash
block_number
}
cursor
}
}
}
}
"""
variables3 = %{ variables3 = %{
"hash" => to_string(address.hash), "hash" => to_string(address.hash),
"first" => 3, "first" => 3,
"after" => last_cursor_page2 "after" => last_cursor_page2
} }
conn = get(conn, "/graphql", query: query3, variables: variables3) conn = get(conn, "/graphql", query: query2, variables: variables3)
%{"data" => %{"address" => %{"transactions" => page3}}} = json_response(conn, 200) %{"data" => %{"address" => %{"transactions" => page3}}} = json_response(conn, 200)

@ -31,5 +31,100 @@ defmodule BlockScoutWeb.Schema.Query.NodeTest do
} }
} }
end end
test "with 'id' for non-existent transaction", %{conn: conn} do
transaction = build(:transaction)
query = """
query($id: ID!) {
node(id: $id) {
... on Transaction {
id
hash
}
}
}
"""
id = Base.encode64("Transaction:#{transaction.hash}")
variables = %{"id" => id}
conn = get(conn, "/graphql", query: query, variables: variables)
%{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == "Transaction not found."
end
test "with valid argument 'id' for an internal transaction", %{conn: conn} do
transaction = insert(:transaction)
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 0)
query = """
query($id: ID!) {
node(id: $id) {
... on InternalTransaction {
id
transaction_hash
index
}
}
}
"""
id =
%{transaction_hash: to_string(transaction.hash), index: internal_transaction.index}
|> Jason.encode!()
|> (fn unique_id -> "InternalTransaction:#{unique_id}" end).()
|> Base.encode64()
variables = %{"id" => id}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"node" => %{
"id" => id,
"transaction_hash" => to_string(transaction.hash),
"index" => internal_transaction.index
}
}
}
end
test "with 'id' for non-existent internal transaction", %{conn: conn} do
transaction = build(:transaction)
internal_transaction = build(:internal_transaction, transaction: transaction, index: 0)
query = """
query($id: ID!) {
node(id: $id) {
... on InternalTransaction {
id
transaction_hash
index
}
}
}
"""
id =
%{transaction_hash: to_string(transaction.hash), index: internal_transaction.index}
|> Jason.encode!()
|> (fn unique_id -> "InternalTransaction:#{unique_id}" end).()
|> Base.encode64()
variables = %{"id" => id}
conn = get(conn, "/graphql", query: query, variables: variables)
%{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == "Internal transaction not found."
end
end end
end end

@ -81,7 +81,7 @@ defmodule BlockScoutWeb.Schema.Query.TransactionTest do
conn = get(conn, "/graphql", query: query, variables: variables) conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200) assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Transaction hash #{transaction.hash} was not found) assert error["message"] == "Transaction not found."
end end
test "errors if argument 'hash' is missing", %{conn: conn} do test "errors if argument 'hash' is missing", %{conn: conn} do
@ -116,4 +116,396 @@ defmodule BlockScoutWeb.Schema.Query.TransactionTest do
assert error["message"] =~ ~s(Argument "hash" has invalid value) assert error["message"] =~ ~s(Argument "hash" has invalid value)
end end
end end
describe "transaction internal_transactions field" do
test "returns all expected internal_transaction fields", %{conn: conn} do
address = insert(:address)
contract_address = insert(:contract_address)
block = insert(:block)
transaction =
:transaction
|> insert(from_address: address)
|> with_contract_creation(contract_address)
|> with_block(block)
internal_transaction_attributes = %{
transaction: transaction,
index: 0,
from_address: address,
call_type: :call
}
internal_transaction =
:internal_transaction_create
|> insert(internal_transaction_attributes)
|> with_contract_creation(contract_address)
query = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
call_type
created_contract_code
error
gas
gas_used
index
init
input
output
trace_address
type
value
block_number
transaction_index
created_contract_address_hash
from_address_hash
to_address_hash
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"first" => 1
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"transaction" => %{
"internal_transactions" => %{
"edges" => [
%{
"node" => %{
"call_type" => internal_transaction.call_type |> to_string() |> String.upcase(),
"created_contract_code" => to_string(internal_transaction.created_contract_code),
"error" => internal_transaction.error,
"gas" => to_string(internal_transaction.gas),
"gas_used" => to_string(internal_transaction.gas_used),
"index" => internal_transaction.index,
"init" => to_string(internal_transaction.init),
"input" => nil,
"output" => nil,
"trace_address" => Jason.encode!(internal_transaction.trace_address),
"type" => internal_transaction.type |> to_string() |> String.upcase(),
"value" => to_string(internal_transaction.value.value),
"block_number" => internal_transaction.block_number,
"transaction_index" => internal_transaction.transaction_index,
"created_contract_address_hash" =>
to_string(internal_transaction.created_contract_address_hash),
"from_address_hash" => to_string(internal_transaction.from_address_hash),
"to_address_hash" => nil,
"transaction_hash" => to_string(internal_transaction.transaction_hash)
}
}
]
}
}
}
}
end
test "with transaction with zero internal transactions", %{conn: conn} do
address = insert(:address)
block = insert(:block)
transaction =
:transaction
|> insert(from_address: address)
|> with_block(block)
query = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"first" => 1
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"transaction" => %{
"internal_transactions" => %{
"edges" => []
}
}
}
}
end
test "internal transactions are ordered by ascending index", %{conn: conn} do
transaction = insert(:transaction)
insert(:internal_transaction, transaction: transaction, index: 2)
insert(:internal_transaction, transaction: transaction, index: 0)
insert(:internal_transaction, transaction: transaction, index: 1)
query = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"first" => 3
}
response =
conn
|> get("/graphql", query: query, variables: variables)
|> json_response(200)
internal_transactions = get_in(response, ["data", "transaction", "internal_transactions", "edges"])
index_order = Enum.map(internal_transactions, & &1["node"]["index"])
assert index_order == Enum.sort(index_order)
end
test "complexity correlates to first or last argument", %{conn: conn} do
transaction = insert(:transaction)
query1 = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables1 = %{
"hash" => to_string(transaction.hash),
"first" => 55
}
response1 =
conn
|> get("/graphql", query: query1, variables: variables1)
|> json_response(200)
assert %{"errors" => [error1, error2, error3]} = response1
assert error1["message"] =~ ~s(Field internal_transactions is too complex)
assert error2["message"] =~ ~s(Field transaction is too complex)
assert error3["message"] =~ ~s(Operation is too complex)
query2 = """
query ($hash: FullHash!, $last: Int!, $count: Int!) {
transaction(hash: $hash) {
internal_transactions(last: $last, count: $count) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables2 = %{
"hash" => to_string(transaction.hash),
"last" => 55,
"count" => 100
}
response2 =
conn
|> get("/graphql", query: query2, variables: variables2)
|> json_response(200)
assert %{"errors" => [error1, error2, error3]} = response2
assert error1["message"] =~ ~s(Field internal_transactions is too complex)
assert error2["message"] =~ ~s(Field transaction is too complex)
assert error3["message"] =~ ~s(Operation is too complex)
end
test "with 'last' and 'count' arguments", %{conn: conn} do
# "`last: N` must always be acompanied by either a `before:` argument to
# the query, or an explicit `count:` option to the `from_query` call.
# Otherwise it is impossible to derive the required offset."
# https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#from_query/4
#
# This test ensures support for a 'count' argument.
transaction = insert(:transaction)
insert(:internal_transaction, transaction: transaction, index: 2)
insert(:internal_transaction, transaction: transaction, index: 0)
insert(:internal_transaction, transaction: transaction, index: 1)
query = """
query ($hash: FullHash!, $last: Int!, $count: Int!) {
transaction(hash: $hash) {
internal_transactions(last: $last, count: $count) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"last" => 1,
"count" => 3
}
[internal_transaction] =
conn
|> get("/graphql", query: query, variables: variables)
|> json_response(200)
|> get_in(["data", "transaction", "internal_transactions", "edges"])
assert internal_transaction["node"]["index"] == 2
end
test "pagination support with 'first' and 'after' arguments", %{conn: conn} do
transaction = insert(:transaction)
for index <- 0..5 do
insert(:internal_transaction_create, transaction: transaction, index: index)
end
query1 = """
query ($hash: AddressHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
index
transaction_hash
}
cursor
}
}
}
}
"""
variables1 = %{
"hash" => to_string(transaction.hash),
"first" => 2
}
conn = get(conn, "/graphql", query: query1, variables: variables1)
%{"data" => %{"transaction" => %{"internal_transactions" => page1}}} = json_response(conn, 200)
assert page1["page_info"] == %{"has_next_page" => true, "has_previous_page" => false}
assert Enum.all?(page1["edges"], &(&1["node"]["index"] in 0..1))
last_cursor_page1 =
page1
|> Map.get("edges")
|> List.last()
|> Map.get("cursor")
query2 = """
query ($hash: AddressHash!, $first: Int!, $after: ID!) {
transaction(hash: $hash) {
internal_transactions(first: $first, after: $after) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
index
transaction_hash
}
cursor
}
}
}
}
"""
variables2 = %{
"hash" => to_string(transaction.hash),
"first" => 2,
"after" => last_cursor_page1
}
page2 =
conn
|> get("/graphql", query: query2, variables: variables2)
|> json_response(200)
|> get_in(["data", "transaction", "internal_transactions"])
assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true}
assert Enum.all?(page2["edges"], &(&1["node"]["index"] in 2..3))
last_cursor_page2 =
page2
|> Map.get("edges")
|> List.last()
|> Map.get("cursor")
variables3 = %{
"hash" => to_string(transaction.hash),
"first" => 2,
"after" => last_cursor_page2
}
page3 =
conn
|> get("/graphql", query: query2, variables: variables3)
|> json_response(200)
|> get_in(["data", "transaction", "internal_transactions"])
assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true}
assert Enum.all?(page3["edges"], &(&1["node"]["index"] in 4..5))
end
end
end end

@ -5,13 +5,28 @@ defmodule Explorer.GraphQL do
import Ecto.Query, import Ecto.Query,
only: [ only: [
from: 2,
order_by: 3, order_by: 3,
or_where: 3, or_where: 3,
where: 3 where: 3
] ]
alias Explorer.Chain.{Address, Hash, Transaction} alias Explorer.Chain.{
Address,
Hash,
InternalTransaction,
Transaction
}
alias Explorer.{Chain, Repo}
@doc """
Returns a query to fetch transactions with a matching `to_address_hash`,
`from_address_hash`, or `created_contract_address_hash` field for a given address.
Orders transactions by descending block number and index.
"""
@spec address_to_transactions_query(Address.t()) :: Ecto.Query.t()
def address_to_transactions_query(%Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}) do def address_to_transactions_query(%Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}) do
Transaction Transaction
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index) |> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
@ -19,4 +34,37 @@ defmodule Explorer.GraphQL do
|> or_where([transaction], transaction.from_address_hash == ^address_hash) |> or_where([transaction], transaction.from_address_hash == ^address_hash)
|> or_where([transaction], transaction.created_contract_address_hash == ^address_hash) |> or_where([transaction], transaction.created_contract_address_hash == ^address_hash)
end end
@doc """
Returns an internal transaction for a given transaction hash and index.
"""
@spec get_internal_transaction(map()) :: {:ok, InternalTransaction.t()} | {:error, String.t()}
def get_internal_transaction(%{transaction_hash: _, index: _} = clauses) do
if internal_transaction = Repo.get_by(InternalTransaction, clauses) do
{:ok, internal_transaction}
else
{:error, "Internal transaction not found."}
end
end
@doc """
Returns a query to fetch internal transactions for a given transaction.
Orders internal transactions by ascending index.
"""
@spec transaction_to_internal_transactions_query(Transaction.t()) :: Ecto.Query.t()
def transaction_to_internal_transactions_query(%Transaction{
hash: %Hash{byte_count: unquote(Hash.Full.byte_count())} = hash
}) do
query =
from(
it in InternalTransaction,
inner_join: t in assoc(it, :transaction),
order_by: [asc: it.index],
where: it.transaction_hash == ^hash,
select: it
)
Chain.where_transaction_has_multiple_internal_transactions(query)
end
end end

@ -87,4 +87,98 @@ defmodule Explorer.GraphQLTest do
assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2)) assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2))
end end
end end
describe "get_internal_transaction/1" do
test "returns existing internal transaction" do
transaction = insert(:transaction)
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 0)
clauses = %{transaction_hash: transaction.hash, index: internal_transaction.index}
{:ok, found_internal_transaction} = GraphQL.get_internal_transaction(clauses)
assert found_internal_transaction.transaction_hash == transaction.hash
assert found_internal_transaction.index == internal_transaction.index
end
test "returns error tuple for non-existent internal transaction" do
transaction = build(:transaction)
internal_transaction = build(:internal_transaction, transaction: transaction, index: 0)
clauses = %{transaction_hash: transaction.hash, index: internal_transaction.index}
assert GraphQL.get_internal_transaction(clauses) == {:error, "Internal transaction not found."}
end
end
describe "transcation_to_internal_transactions_query/1" do
test "with transaction with one internal transaction" do
transaction1 = insert(:transaction)
transaction2 = insert(:transaction)
internal_transaction = insert(:internal_transaction_create, transaction: transaction1, index: 0)
insert(:internal_transaction_create, transaction: transaction2, index: 0)
[found_internal_transaction] =
transaction1
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
assert found_internal_transaction.transaction_hash == transaction1.hash
assert found_internal_transaction.index == internal_transaction.index
end
test "with transaction with multiple internal transactions" do
transaction1 = insert(:transaction)
transaction2 = insert(:transaction)
for index <- 0..2 do
insert(:internal_transaction_create, transaction: transaction1, index: index)
end
insert(:internal_transaction_create, transaction: transaction2, index: 0)
found_internal_transactions =
transaction1
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
assert length(found_internal_transactions) == 3
for found_internal_transaction <- found_internal_transactions do
assert found_internal_transaction.transaction_hash == transaction1.hash
end
end
test "orders internal transactions by ascending index" do
transaction = insert(:transaction)
insert(:internal_transaction_create, transaction: transaction, index: 2)
insert(:internal_transaction_create, transaction: transaction, index: 0)
insert(:internal_transaction_create, transaction: transaction, index: 1)
found_internal_transactions =
transaction
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
index_order = Enum.map(found_internal_transactions, & &1.index)
assert index_order == Enum.sort(index_order)
end
# Note that `transaction_to_internal_transactions_query/1` relies on
# `Explorer.Chain.where_transaction_has_multiple_transactions/1` to ensure the
# following behavior:
#
# * exclude internal transactions of type call with no siblings in the
# transaction
#
# * include internal transactions of type create, reward, or suicide
# even when they are alone in the parent transaction
#
# These two requirements are tested in `Explorer.ChainTest`.
end
end end

Loading…
Cancel
Save