From e7071172e251caebfc770c9fbf7e571332b8d426 Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Mon, 3 Dec 2018 10:58:12 -0500 Subject: [PATCH 1/3] Allows other sites to open blockscout in iframe Why: * For our partners to be able to open BlockScout in an iframe. * Issue link: n/ah This change addresses the need by: * Creating `AllowIframe` plug. * Editing router to have a new scope for write-only routes and a separate one for read-only routes which allows for iframes (using the plug mentioned above). --- .../lib/block_scout_web/plug/allow_iframe.ex | 14 ++++++++++++++ apps/block_scout_web/lib/block_scout_web/router.ex | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex diff --git a/apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex b/apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex new file mode 100644 index 0000000000..ee20311efc --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex @@ -0,0 +1,14 @@ +defmodule BlockScoutWeb.Plug.AllowIframe do + @moduledoc """ + Allows for iframes by deleting the + [`X-Frame-Options` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) + """ + + alias Plug.Conn + + def init(opts), do: opts + + def call(conn, _opts) do + Conn.delete_resp_header(conn, "x-frame-options") + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 577a414163..b5825d44f5 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -55,8 +55,14 @@ defmodule BlockScoutWeb.Router do max_complexity: @max_complexity ) + # Disallows Iframes (write routes) scope "/", BlockScoutWeb do pipe_through(:browser) + end + + # Allows Iframes (read-only routes) + scope "/", BlockScoutWeb do + pipe_through([:browser, BlockScoutWeb.Plug.AllowIframe]) resources("/", ChainController, only: [:show], singleton: true, as: :chain) From fa09f399d8e456645bee835cd4763fd91e2a565d Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Tue, 4 Dec 2018 14:16:31 -0500 Subject: [PATCH 2/3] GraphQL API token_transfers query Why: * For GraphQL API users to be able to fetch token transfer events. RPC API users could use this instead of the `tokentx` RPC API action. Example usage: 1. ``` query ($token_contract_address_hash: AddressHash!, $first: Int!) { token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { edges { node { amount block_number log_index token_id from_address_hash to_address_hash token_contract_address_hash transaction_hash } } } } ``` 2. with pagination support via [Relay Cursor Connections](https://facebook.github.io/relay/graphql/connections.htm) ``` query ($token_contract_address_hash: AddressHash!, $first: Int!, $after: ID!) { token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first, after: $after) { page_info { has_next_page has_previous_page } edges { node { amount from_address_hash to_address_hash } cursor } } } ``` * Issue link: n/a This change addresses the need by: * Editing the node interface and node field in `BlockScoutWeb.Schema` to support token transfers. * Adding `token_transfers` connection field to root query object in `BlockScoutWeb.Schema` * Making `token_transfer` object types a node type and adding a few new fields to it, in `BlockScoutWeb.Schema.Types`. * Creating `BlockScoutWeb.Resolvers.TokenTransfer` with resolver functions for getting token transfers. * Adding `get_token_transfer/1` and `list_token_transfers_query/1` to `Explorer.GraphQL`. These two functions are used by the new resolver functions mentioned above. * Editing RPC API docs to reference the new GraphQL query field added in this commit. --- .../lib/block_scout_web/etherscan.ex | 3 +- .../resolvers/token_transfer.ex | 24 ++ .../lib/block_scout_web/schema.ex | 40 ++- .../lib/block_scout_web/schema/types.ex | 26 +- .../schema/query/node_test.exs | 67 ++++ .../schema/query/token_transfers_test.exs | 326 ++++++++++++++++++ apps/explorer/lib/explorer/graphql.ex | 29 ++ apps/explorer/test/explorer/graphql_test.exs | 125 +++++++ 8 files changed, 623 insertions(+), 17 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex create mode 100644 apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 9ed0c1f64e..e8b14869cc 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -1087,7 +1087,8 @@ defmodule BlockScoutWeb.Etherscan do @account_tokentx_action %{ name: "tokentx", - description: "Get token transfer events by address. Up to a maximum of 10,000 token transfer events.", + description: + "Get token transfer events by address. Up to a maximum of 10,000 token transfer events. Also available through a GraphQL 'token_transfers' query.", required_params: [ %{ key: "address", diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex new file mode 100644 index 0000000000..38b8b3f4ac --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Resolvers.TokenTransfer do + @moduledoc false + + alias Absinthe.Relay.Connection + alias Explorer.{GraphQL, Repo} + + def get_by(%{transaction_hash: _, log_index: _} = args) do + GraphQL.get_token_transfer(args) + end + + def get_by(_, %{token_contract_address_hash: token_contract_address_hash} = args, _) do + connection_args = Map.take(args, [:after, :before, :first, :last]) + + token_contract_address_hash + |> GraphQL.list_token_transfers_query() + |> Connection.from_query(&Repo.all/1, connection_args, options(args)) + end + + defp options(%{before: _}), do: [] + + defp options(%{count: count}), do: [count: count] + + defp options(_), do: [] +end diff --git a/apps/block_scout_web/lib/block_scout_web/schema.ex b/apps/block_scout_web/lib/block_scout_web/schema.ex index 7d6d150acd..42d644c418 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema.ex @@ -11,23 +11,28 @@ defmodule BlockScoutWeb.Schema do Address, Block, InternalTransaction, + TokenTransfer, Transaction } alias Explorer.Chain alias Explorer.Chain.InternalTransaction, as: ExplorerChainInternalTransaction + alias Explorer.Chain.TokenTransfer, as: ExplorerChainTokenTransfer alias Explorer.Chain.Transaction, as: ExplorerChainTransaction import_types(BlockScoutWeb.Schema.Types) node interface do resolve_type(fn - %ExplorerChainTransaction{}, _ -> - :transaction - %ExplorerChainInternalTransaction{}, _ -> :internal_transaction + %ExplorerChainTokenTransfer{}, _ -> + :token_transfer + + %ExplorerChainTransaction{}, _ -> + :transaction + _, _ -> nil end) @@ -36,15 +41,20 @@ defmodule BlockScoutWeb.Schema do query do node field do resolve(fn - %{type: :transaction, id: transaction_hash_string}, _ -> - {:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string) - 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}) + %{type: :token_transfer, id: id}, _ -> + %{"transaction_hash" => transaction_hash_string, "log_index" => log_index} = Jason.decode!(id) + {:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string) + TokenTransfer.get_by(%{transaction_hash: transaction_hash, log_index: log_index}) + + %{type: :transaction, id: transaction_hash_string}, _ -> + {:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string) + Transaction.get_by(%{}, %{hash: hash}, %{}) + _, _ -> {:error, "Unknown node"} end) @@ -69,6 +79,22 @@ defmodule BlockScoutWeb.Schema do resolve(&Block.get_by/3) end + @desc "Gets token transfers by token contract address hash." + connection field(:token_transfers, node_type: :token_transfer) do + arg(:token_contract_address_hash, non_null(:address_hash)) + arg(:count, :integer) + + resolve(&TokenTransfer.get_by/3) + + complexity(fn + %{first: first}, child_complexity -> + first * child_complexity + + %{last: last}, child_complexity -> + last * child_complexity + end) + end + @desc "Gets a transaction by hash." field :transaction, :transaction do arg(:hash, non_null(:full_hash)) diff --git a/apps/block_scout_web/lib/block_scout_web/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/schema/types.ex index 85e843c447..029b322f9c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/types.ex @@ -16,6 +16,7 @@ defmodule BlockScoutWeb.Schema.Types do connection(node_type: :transaction) connection(node_type: :internal_transaction) + connection(node_type: :token_transfer) @desc """ A stored representation of a Web3 address. @@ -105,6 +106,20 @@ defmodule BlockScoutWeb.Schema.Types do field(:address_hash, :address_hash) end + @desc """ + Represents a token transfer between addresses. + """ + node object(:token_transfer, id_fetcher: &token_transfer_id_fetcher/2) do + field(:amount, :decimal) + field(:block_number, :integer) + field(:log_index, :integer) + field(:token_id, :decimal) + field(:from_address_hash, :address_hash) + field(:to_address_hash, :address_hash) + field(:token_contract_address_hash, :address_hash) + field(:transaction_hash, :full_hash) + end + @desc """ Models a Web3 transaction. """ @@ -142,15 +157,8 @@ defmodule BlockScoutWeb.Schema.Types do end end - @desc """ - Represents a token transfer between addresses. - """ - object :token_transfer do - field(:amount, :decimal) - field(:from_address_hash, :address_hash) - field(:to_address_hash, :address_hash) - field(:token_contract_address_hash, :address_hash) - field(:transaction_hash, :full_hash) + def token_transfer_id_fetcher(%{transaction_hash: transaction_hash, log_index: log_index}, _) do + Jason.encode!(%{transaction_hash: to_string(transaction_hash), log_index: log_index}) end def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash) diff --git a/apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs b/apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs index 00d21fe85d..a8330f085a 100644 --- a/apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs +++ b/apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs @@ -126,5 +126,72 @@ defmodule BlockScoutWeb.Schema.Query.NodeTest do assert error["message"] == "Internal transaction not found." end + + test "with valid argument 'id' for a token_transfer", %{conn: conn} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on TokenTransfer { + id + transaction_hash + log_index + } + } + } + """ + + id = + %{transaction_hash: to_string(token_transfer.transaction_hash), log_index: token_transfer.log_index} + |> Jason.encode!() + |> (fn unique_id -> "TokenTransfer:#{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(token_transfer.transaction_hash), + "log_index" => token_transfer.log_index + } + } + } + end + + test "with id for non-existent token transfer", %{conn: conn} do + transaction = build(:transaction) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on TokenTransfer { + id + transaction_hash + log_index + } + } + } + """ + + id = + %{transaction_hash: to_string(transaction.hash), log_index: 0} + |> Jason.encode!() + |> (fn unique_id -> "TokenTransfer:#{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"] == "Token transfer not found." + end end end diff --git a/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs b/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs new file mode 100644 index 0000000000..34c1cdfb1f --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs @@ -0,0 +1,326 @@ +defmodule BlockScoutWeb.Schema.Query.TokenTransfersTest do + use BlockScoutWeb.ConnCase + + describe "token_transfers field" do + test "with valid argument, returns all expected fields", %{conn: conn} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + address_hash = to_string(token_transfer.token_contract_address_hash) + + query = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + edges { + node { + amount + block_number + log_index + token_id + from_address_hash + to_address_hash + token_contract_address_hash + transaction_hash + } + } + } + } + """ + + variables = %{ + "token_contract_address_hash" => address_hash, + "first" => 1 + } + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "token_transfers" => %{ + "edges" => [ + %{ + "node" => %{ + "amount" => to_string(token_transfer.amount), + "block_number" => token_transfer.block_number, + "log_index" => token_transfer.log_index, + "token_id" => token_transfer.token_id, + "from_address_hash" => to_string(token_transfer.from_address_hash), + "to_address_hash" => to_string(token_transfer.to_address_hash), + "token_contract_address_hash" => to_string(token_transfer.token_contract_address_hash), + "transaction_hash" => to_string(token_transfer.transaction_hash) + } + } + ] + } + } + } + end + + test "with token contract address with zero token transfers", %{conn: conn} do + address = insert(:contract_address) + + query = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + edges { + node { + amount + block_number + log_index + token_id + from_address_hash + to_address_hash + token_contract_address_hash + transaction_hash + } + } + } + } + """ + + variables = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 10 + } + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "token_transfers" => %{ + "edges" => [] + } + } + } + end + + test "complexity correlates to first or last argument", %{conn: conn} do + address = insert(:contract_address) + + query1 = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + edges { + node { + amount + from_address_hash + to_address_hash + } + } + } + } + """ + + variables1 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 55 + } + + response1 = + conn + |> get("/graphql", query: query1, variables: variables1) + |> json_response(200) + + %{"errors" => [response1_error1, response1_error2]} = response1 + + assert response1_error1["message"] =~ ~s(Field token_transfers is too complex) + assert response1_error2["message"] =~ ~s(Operation is too complex) + + query2 = """ + query ($token_contract_address_hash: AddressHash!, $last: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, last: $last) { + edges { + node { + amount + from_address_hash + to_address_hash + } + } + } + } + """ + + variables2 = %{ + "token_contract_address_hash" => to_string(address.hash), + "last" => 55 + } + + response2 = + conn + |> get("/graphql", query: query2, variables: variables2) + |> json_response(200) + + %{"errors" => [response2_error1, response2_error2]} = response2 + assert response2_error1["message"] =~ ~s(Field token_transfers is too complex) + assert response2_error2["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. + + address = insert(:contract_address) + + blocks = insert_list(2, :block) + + [transaction1, transaction2] = + for block <- blocks do + :transaction + |> insert() + |> with_block(block) + end + + token_transfer_attrs1 = %{ + block_number: transaction1.block_number, + transaction: transaction1, + token_contract_address: address + } + + token_transfer_attrs2 = %{ + block_number: transaction2.block_number, + transaction: transaction2, + token_contract_address: address + } + + insert(:token_transfer, token_transfer_attrs1) + insert(:token_transfer, token_transfer_attrs2) + + query = """ + query ($token_contract_address_hash: AddressHash!, $last: Int!, $count: Int) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, last: $last, count: $count) { + edges { + node { + transaction_hash + } + } + } + } + """ + + variables = %{ + "token_contract_address_hash" => to_string(address.hash), + "last" => 1, + "count" => 2 + } + + [token_transfer] = + conn + |> get("/graphql", query: query, variables: variables) + |> json_response(200) + |> get_in(["data", "token_transfers", "edges"]) + + assert token_transfer["node"]["transaction_hash"] == to_string(transaction1.hash) + end + + test "pagination support with 'first' and 'after' arguments", %{conn: conn} do + address = insert(:contract_address) + + blocks = insert_list(3, :block) + + [transaction1, transaction2, transaction3] = + transactions = + for block <- blocks do + :transaction + |> insert() + |> with_block(block) + end + + for transaction <- transactions do + token_transfer_attrs = %{ + block_number: transaction.block_number, + transaction: transaction, + token_contract_address: address + } + + insert(:token_transfer, token_transfer_attrs) + end + + query1 = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + transaction_hash + } + cursor + } + } + } + """ + + variables1 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 1 + } + + conn = get(conn, "/graphql", query: query1, variables: variables1) + + %{"data" => %{"token_transfers" => page1}} = json_response(conn, 200) + + assert page1["page_info"] == %{"has_next_page" => true, "has_previous_page" => false} + assert Enum.all?(page1["edges"], &(&1["node"]["transaction_hash"] == to_string(transaction3.hash))) + + last_cursor_page1 = + page1 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + query2 = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!, $after: ID!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first, after: $after) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + transaction_hash + } + cursor + } + } + } + """ + + variables2 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 1, + "after" => last_cursor_page1 + } + + conn = get(conn, "/graphql", query: query2, variables: variables2) + + %{"data" => %{"token_transfers" => page2}} = json_response(conn, 200) + + assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true} + assert Enum.all?(page2["edges"], &(&1["node"]["transaction_hash"] == to_string(transaction2.hash))) + + last_cursor_page2 = + page2 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + variables3 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 1, + "after" => last_cursor_page2 + } + + conn = get(conn, "/graphql", query: query2, variables: variables3) + + %{"data" => %{"token_transfers" => page3}} = json_response(conn, 200) + + assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true} + assert Enum.all?(page3["edges"], &(&1["node"]["transaction_hash"] == to_string(transaction1.hash))) + end + end +end diff --git a/apps/explorer/lib/explorer/graphql.ex b/apps/explorer/lib/explorer/graphql.ex index b3265a4150..2ab931f743 100644 --- a/apps/explorer/lib/explorer/graphql.ex +++ b/apps/explorer/lib/explorer/graphql.ex @@ -15,6 +15,7 @@ defmodule Explorer.GraphQL do Address, Hash, InternalTransaction, + TokenTransfer, Transaction } @@ -67,4 +68,32 @@ defmodule Explorer.GraphQL do Chain.where_transaction_has_multiple_internal_transactions(query) end + + @doc """ + Returns a token transfer for a given transaction hash and log index. + """ + @spec get_token_transfer(map()) :: {:ok, TokenTransfer.t()} | {:error, String.t()} + def get_token_transfer(%{transaction_hash: _, log_index: _} = clauses) do + if token_transfer = Repo.get_by(TokenTransfer, clauses) do + {:ok, token_transfer} + else + {:error, "Token transfer not found."} + end + end + + @doc """ + Returns a query to fetch token transfers for a token contract address hash. + + Orders token transfers by descending block number, descending transaction index, and ascending log index. + """ + @spec list_token_transfers_query(Hash.t()) :: Ecto.Query.t() + def list_token_transfers_query(%Hash{byte_count: unquote(Hash.Address.byte_count())} = token_contract_address_hash) do + from( + tt in TokenTransfer, + inner_join: t in assoc(tt, :transaction), + where: tt.token_contract_address_hash == ^token_contract_address_hash, + order_by: [desc: tt.block_number, desc: t.index, asc: tt.log_index], + select: tt + ) + end end diff --git a/apps/explorer/test/explorer/graphql_test.exs b/apps/explorer/test/explorer/graphql_test.exs index f98a030bb2..ef0dce974d 100644 --- a/apps/explorer/test/explorer/graphql_test.exs +++ b/apps/explorer/test/explorer/graphql_test.exs @@ -181,4 +181,129 @@ defmodule Explorer.GraphQLTest do # # These two requirements are tested in `Explorer.ChainTest`. end + + describe "get_token_transfer/1" do + test "returns existing token transfer" do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + + clauses = %{transaction_hash: token_transfer.transaction_hash, log_index: token_transfer.log_index} + + {:ok, found_token_transfer} = GraphQL.get_token_transfer(clauses) + + assert found_token_transfer.transaction_hash == token_transfer.transaction_hash + assert found_token_transfer.log_index == token_transfer.log_index + end + + test " returns error tuple for non-existing token transfer" do + transaction = insert(:transaction) + token_transfer = build(:token_transfer, transaction: transaction) + + clauses = %{transaction_hash: transaction.hash, log_index: token_transfer.log_index} + + assert GraphQL.get_token_transfer(clauses) == {:error, "Token transfer not found."} + end + end + + describe "list_token_transfers_query/1" do + test "with token contract address hash with zero token transfers" do + result = + :address + |> insert() + |> Map.get(:hash) + |> GraphQL.list_token_transfers_query() + |> Repo.all() + + assert result == [] + end + + test "returns all expected token transfer fields" do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + + [found_token_transfer] = + token_transfer.token_contract_address_hash + |> GraphQL.list_token_transfers_query() + |> Repo.all() + + expected_fields = ~w( + amount + block_number + log_index + token_id + from_address_hash + to_address_hash + token_contract_address_hash + transaction_hash + )a + + for expected_field <- expected_fields do + assert Map.get(found_token_transfer, expected_field) == Map.get(token_transfer, expected_field) + end + end + + test "orders token transfers by descending block number, descending transaction index, and ascending log index" do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + + transactions_block2 = + 2 + |> insert_list(:transaction) + |> with_block(second_block) + + transactions_block3 = + 2 + |> insert_list(:transaction) + |> with_block(third_block) + + transactions_block1 = + 2 + |> insert_list(:transaction) + |> with_block(first_block) + + all_transactions = Enum.concat([transactions_block2, transactions_block3, transactions_block1]) + + token_address = insert(:contract_address) + insert(:token, contract_address: token_address) + + for transaction <- all_transactions do + token_transfer_attrs1 = %{ + block_number: transaction.block_number, + log_index: 0, + transaction: transaction, + token_contract_address: token_address + } + + token_transfer_attrs2 = %{ + block_number: transaction.block_number, + log_index: 1, + transaction: transaction, + token_contract_address: token_address + } + + insert(:token_transfer, token_transfer_attrs1) + insert(:token_transfer, token_transfer_attrs2) + end + + found_token_transfers = + token_address.hash + |> GraphQL.list_token_transfers_query() + |> Repo.all() + |> Repo.preload(:transaction) + + block_number_order = Enum.map(found_token_transfers, & &1.block_number) + transaction_index_order = Enum.map(found_token_transfers, &{&1.block_number, &1.transaction.index}) + + assert block_number_order == Enum.sort(block_number_order, &(&1 >= &2)) + assert transaction_index_order == Enum.sort(transaction_index_order, &(&1 >= &2)) + + tt_by_block_transaction = Enum.group_by(found_token_transfers, &{&1.block_number, &1.transaction.index}) + + for {_, token_transfers} <- tt_by_block_transaction do + log_index_order = Enum.map(token_transfers, & &1.log_index) + assert log_index_order == Enum.sort(log_index_order) + end + end + end end From 501734797da70483397f5e79185616406bb9ddc7 Mon Sep 17 00:00:00 2001 From: Gustavo Santos Ferreira Date: Wed, 5 Dec 2018 11:17:18 -0200 Subject: [PATCH 3/3] change the test file to exs --- ...inventory_controller_test.ex => inventory_controller_test.exs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/block_scout_web/test/block_scout_web/controllers/tokens/{inventory_controller_test.ex => inventory_controller_test.exs} (100%) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.ex b/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs similarity index 100% rename from apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.ex rename to apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs