Merge branch 'master' into show-unit-price-with-balance

pull/1113/head
Devin Turner 6 years ago committed by GitHub
commit 495ffb2ddb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  2. 7
      apps/block_scout_web/lib/block_scout_web/resolvers/address.ex
  3. 16
      apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex
  4. 29
      apps/block_scout_web/lib/block_scout_web/schema.ex
  5. 22
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  6. 2
      apps/block_scout_web/mix.exs
  7. 493
      apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs
  8. 185
      apps/block_scout_web/test/block_scout_web/schema/query/addresses_test.exs
  9. 35
      apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs
  10. 2
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex
  11. 60
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/parity_test.exs
  12. 22
      apps/explorer/lib/explorer/graphql.ex
  13. 90
      apps/explorer/test/explorer/graphql_test.exs
  14. 1
      mix.lock

@ -925,7 +925,8 @@ defmodule BlockScoutWeb.Etherscan do
@account_txlist_action %{ @account_txlist_action %{
name: "txlist", name: "txlist",
description: "Get transactions by address. Up to a maximum of 10,000 transactions.", description:
"Get transactions by address. Up to a maximum of 10,000 transactions. Also available through a GraphQL 'address' query.",
required_params: [ required_params: [
%{ %{
key: "address", key: "address",

@ -9,4 +9,11 @@ defmodule BlockScoutWeb.Resolvers.Address do
result -> {:ok, result} result -> {:ok, result}
end end
end end
def get_by(_, %{hash: hash}, _) do
case Chain.hash_to_address(hash) do
{:error, :not_found} -> {:error, "Address not found."}
{:ok, _} = result -> result
end
end
end end

@ -1,7 +1,9 @@
defmodule BlockScoutWeb.Resolvers.Transaction do defmodule BlockScoutWeb.Resolvers.Transaction do
@moduledoc false @moduledoc false
alias Explorer.Chain alias Absinthe.Relay.Connection
alias Explorer.{Chain, GraphQL, Repo}
alias Explorer.Chain.Address
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
@ -9,4 +11,16 @@ defmodule BlockScoutWeb.Resolvers.Transaction do
{:error, :not_found} -> {:error, "Transaction hash #{hash} was not found."} {:error, :not_found} -> {:error, "Transaction hash #{hash} was not found."}
end end
end end
def get_by(%Address{} = address, args, _) do
address
|> GraphQL.address_to_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 end

@ -2,15 +2,44 @@ defmodule BlockScoutWeb.Schema do
@moduledoc false @moduledoc false
use Absinthe.Schema use Absinthe.Schema
use Absinthe.Relay.Schema, :modern
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, Transaction}
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Transaction, as: ExplorerChainTransaction
import_types(BlockScoutWeb.Schema.Types) import_types(BlockScoutWeb.Schema.Types)
node interface do
resolve_type(fn
%ExplorerChainTransaction{}, _ ->
:transaction
_, _ ->
nil
end)
end
query 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}, %{})
_, _ ->
{:error, "Unknown node"}
end)
end
@desc "Gets an address by hash."
field :address, :address do
arg(:hash, non_null(:address_hash))
resolve(&Address.get_by/3)
end
@desc "Gets addresses by address hash." @desc "Gets addresses by address hash."
field :addresses, list_of(:address) do field :addresses, list_of(:address) do
arg(:hashes, non_null(list_of(non_null(:address_hash)))) arg(:hashes, non_null(list_of(non_null(:address_hash))))

@ -2,12 +2,17 @@ defmodule BlockScoutWeb.Schema.Types do
@moduledoc false @moduledoc false
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
use Absinthe.Relay.Schema.Notation, :modern
import Absinthe.Resolution.Helpers import Absinthe.Resolution.Helpers
alias BlockScoutWeb.Resolvers.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)
@desc """ @desc """
A stored representation of a Web3 address. A stored representation of a Web3 address.
""" """
@ -20,6 +25,19 @@ defmodule BlockScoutWeb.Schema.Types do
field :smart_contract, :smart_contract do field :smart_contract, :smart_contract do
resolve(dataloader(:db, :smart_contract)) resolve(dataloader(:db, :smart_contract))
end end
connection field(:transactions, node_type: :transaction) do
arg(:count, :integer)
resolve(&Transaction.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 """
@ -62,7 +80,7 @@ defmodule BlockScoutWeb.Schema.Types do
@desc """ @desc """
Models a Web3 transaction. Models a Web3 transaction.
""" """
object :transaction do node object(:transaction, id_fetcher: &transaction_id_fetcher/2) do
field(:hash, :full_hash) field(:hash, :full_hash)
field(:block_number, :integer) field(:block_number, :integer)
field(:cumulative_gas_used, :decimal) field(:cumulative_gas_used, :decimal)
@ -93,4 +111,6 @@ defmodule BlockScoutWeb.Schema.Types do
field(:token_contract_address_hash, :address_hash) field(:token_contract_address_hash, :address_hash)
field(:transaction_hash, :full_hash) field(:transaction_hash, :full_hash)
end end
def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash)
end end

@ -65,6 +65,8 @@ defmodule BlockScoutWeb.Mixfile do
{:absinthe_phoenix, "~> 1.4"}, {:absinthe_phoenix, "~> 1.4"},
# Plug support for Absinthe # Plug support for Absinthe
{:absinthe_plug, "~> 1.4"}, {:absinthe_plug, "~> 1.4"},
# Absinthe support for the Relay framework
{:absinthe_relay, "~> 1.4"},
{:bypass, "~> 0.8", only: :test}, {:bypass, "~> 0.8", only: :test},
{:credo, "0.10.2", only: [:dev, :test], runtime: false}, {:credo, "0.10.2", only: [:dev, :test], runtime: false},
# For Absinthe to load data in batches # For Absinthe to load data in batches

@ -2,12 +2,12 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
use BlockScoutWeb.ConnCase use BlockScoutWeb.ConnCase
describe "address field" do describe "address field" do
test "with valid argument 'hashes', returns all expected fields", %{conn: conn} do test "with valid argument 'hash', returns all expected fields", %{conn: conn} do
address = insert(:address, fetched_coin_balance: 100) address = insert(:address, fetched_coin_balance: 100)
query = """ query = """
query ($hashes: [AddressHash!]!) { query ($hash: AddressHash!) {
addresses(hashes: $hashes) { address(hash: $hash) {
hash hash
fetched_coin_balance fetched_coin_balance
fetched_coin_balance_block_number fetched_coin_balance_block_number
@ -16,20 +16,18 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
} }
""" """
variables = %{"hashes" => to_string(address.hash)} variables = %{"hash" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables) conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{ assert json_response(conn, 200) == %{
"data" => %{ "data" => %{
"addresses" => [ "address" => %{
%{ "hash" => to_string(address.hash),
"hash" => to_string(address.hash), "fetched_coin_balance" => to_string(address.fetched_coin_balance.value),
"fetched_coin_balance" => to_string(address.fetched_coin_balance.value), "fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number,
"fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number, "contract_code" => nil
"contract_code" => nil }
}
]
} }
} }
end end
@ -38,24 +36,22 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
address = insert(:contract_address, fetched_coin_balance: 100) address = insert(:contract_address, fetched_coin_balance: 100)
query = """ query = """
query ($hashes: [AddressHash!]!) { query ($hash: AddressHash!) {
addresses(hashes: $hashes) { address(hash: $hash) {
contract_code contract_code
} }
} }
""" """
variables = %{"hashes" => to_string(address.hash)} variables = %{"hash" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables) conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{ assert json_response(conn, 200) == %{
"data" => %{ "data" => %{
"addresses" => [ "address" => %{
%{ "contract_code" => to_string(address.contract_code)
"contract_code" => to_string(address.contract_code) }
}
]
} }
} }
end end
@ -65,8 +61,8 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
smart_contract = insert(:smart_contract, address_hash: address.hash) smart_contract = insert(:smart_contract, address_hash: address.hash)
query = """ query = """
query ($hashes: [AddressHash!]!) { query ($hash: AddressHash!) {
addresses(hashes: $hashes) { address(hash: $hash) {
fetched_coin_balance fetched_coin_balance
smart_contract { smart_contract {
name name
@ -80,52 +76,50 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
} }
""" """
variables = %{"hashes" => to_string(address.hash)} variables = %{"hash" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables) conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{ assert json_response(conn, 200) == %{
"data" => %{ "data" => %{
"addresses" => [ "address" => %{
%{ "fetched_coin_balance" => to_string(address.fetched_coin_balance.value),
"fetched_coin_balance" => to_string(address.fetched_coin_balance.value), "smart_contract" => %{
"smart_contract" => %{ "name" => smart_contract.name,
"name" => smart_contract.name, "compiler_version" => smart_contract.compiler_version,
"compiler_version" => smart_contract.compiler_version, "optimization" => smart_contract.optimization,
"optimization" => smart_contract.optimization, "contract_source_code" => smart_contract.contract_source_code,
"contract_source_code" => smart_contract.contract_source_code, "abi" => Jason.encode!(smart_contract.abi),
"abi" => Jason.encode!(smart_contract.abi), "address_hash" => to_string(address.hash)
"address_hash" => to_string(address.hash)
}
} }
] }
} }
} }
end end
test "errors for non-existent address hashes", %{conn: conn} do test "errors for non-existent address hash", %{conn: conn} do
address = build(:address) address = build(:address)
query = """ query = """
query ($hashes: [AddressHash!]!) { query ($hash: AddressHash!) {
addresses(hashes: $hashes) { address(hash: $hash) {
fetched_coin_balance fetched_coin_balance
} }
} }
""" """
variables = %{"hashes" => [to_string(address.hash)]} variables = %{"hash" => to_string(address.hash)}
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(Addresses not found.) assert error["message"] =~ ~s(Address not found.)
end end
test "errors if argument 'hashes' is missing", %{conn: conn} do test "errors if argument 'hash' is missing", %{conn: conn} do
query = """ query = """
query { query {
addresses { address {
fetched_coin_balance fetched_coin_balance
} }
} }
@ -136,50 +130,423 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest 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(In argument "hashes": Expected type "[AddressHash!]!", found null.) assert error["message"] == ~s(In argument "hash": Expected type "AddressHash!", found null.)
end end
test "errors if argument 'hashes' is not a list of address hashes", %{conn: conn} do test "errors if argument 'hash' is not a valid address hash", %{conn: conn} do
query = """ query = """
query ($hashes: [AddressHash!]!) { query ($hash: AddressHash!) {
addresses(hashes: $hashes) { address(hash: $hash) {
fetched_coin_balance fetched_coin_balance
} }
} }
""" """
variables = %{"hashes" => ["someInvalidHash"]} variables = %{"hash" => "someInvalidHash"}
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(Argument "hashes" has invalid value) assert error["message"] =~ ~s(Argument "hash" has invalid value)
end end
end
describe "address transactions field" do
test "returns all expected transaction fields", %{conn: conn} do
address = insert(:address)
test "correlates complexity to size of 'hashes' argument", %{conn: conn} do transaction = insert(:transaction, from_address: address)
# max of 50 addresses with four fields of complexity 1 can be fetched
# per query:
# 50 * 4 = 200, which is equal to a max complexity of 200
hashes = 51 |> build_list(:address) |> Enum.map(&to_string(&1.hash))
query = """ query = """
query ($hashes: [AddressHash!]!) { query ($hash: AddressHash!, $first: Int!) {
addresses(hashes: $hashes) { address(hash: $hash) {
hash transactions(first: $first) {
fetched_coin_balance edges {
fetched_coin_balance_block_number node {
contract_code hash
block_number
cumulative_gas_used
error
gas
gas_price
gas_used
index
input
nonce
r
s
status
v
value
from_address_hash
to_address_hash
created_contract_address_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(address.hash),
"first" => 1
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"address" => %{
"transactions" => %{
"edges" => [
%{
"node" => %{
"hash" => to_string(transaction.hash),
"block_number" => transaction.block_number,
"cumulative_gas_used" => nil,
"error" => transaction.error,
"gas" => to_string(transaction.gas),
"gas_price" => to_string(transaction.gas_price.value),
"gas_used" => nil,
"index" => transaction.index,
"input" => to_string(transaction.input),
"nonce" => to_string(transaction.nonce),
"r" => to_string(transaction.r),
"s" => to_string(transaction.s),
"status" => nil,
"v" => transaction.v,
"value" => to_string(transaction.value.value),
"from_address_hash" => to_string(transaction.from_address_hash),
"to_address_hash" => to_string(transaction.to_address_hash),
"created_contract_address_hash" => nil
}
}
]
}
}
}
}
end
test "with address with zero transactions", %{conn: conn} do
address = insert(:address)
query = """
query ($hash: AddressHash!, $first: Int!) {
address(hash: $hash) {
transactions(first: $first) {
edges {
node {
hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(address.hash),
"first" => 1
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"address" => %{
"transactions" => %{
"edges" => []
}
}
}
}
end
test "transactions are ordered by descending block and index", %{conn: conn} do
first_block = insert(:block)
second_block = insert(:block)
third_block = insert(:block)
address = insert(:address)
3
|> insert_list(:transaction, from_address: address)
|> with_block(second_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(third_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(first_block)
query = """
query ($hash: AddressHash!, $first: Int!) {
address(hash: $hash) {
transactions(first: $first) {
edges {
node {
hash
block_number
index
}
}
}
}
}
"""
variables = %{
"hash" => to_string(address.hash),
"first" => 3
}
conn = get(conn, "/graphql", query: query, variables: variables)
%{
"data" => %{
"address" => %{
"transactions" => %{
"edges" => transactions
}
}
}
} = json_response(conn, 200)
block_number_and_index_order =
Enum.map(transactions, fn transaction ->
{transaction["node"]["block_number"], transaction["node"]["index"]}
end)
assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2))
assert length(transactions) == 3
assert Enum.all?(transactions, &(&1["node"]["block_number"] == third_block.number))
end
test "complexity correlates to 'first' or 'last' arguments", %{conn: conn} do
address = build(:address)
query = """
query ($hash: AddressHash!, $first: Int!) {
address(hash: $hash) {
transactions(first: $first) {
edges {
node {
hash
}
}
}
} }
} }
""" """
variables = %{"hashes" => hashes} variables = %{
"hash" => to_string(address.hash),
"first" => 67
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error1, error2, error3]} = json_response(conn, 200)
assert error1["message"] =~ ~s(Field transactions is too complex)
assert error2["message"] =~ ~s(Field address 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 of a 'count' argument.
first_block = insert(:block)
second_block = insert(:block)
third_block = insert(:block)
address = insert(:address)
3
|> insert_list(:transaction, from_address: address)
|> with_block(second_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(third_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(first_block)
query = """
query ($hash: AddressHash!, $last: Int!, $count: Int!) {
address(hash: $hash) {
transactions(last: $last, count: $count) {
edges {
node {
hash
block_number
}
}
}
}
}
"""
variables = %{
"hash" => to_string(address.hash),
"last" => 3,
"count" => 9
}
conn = get(conn, "/graphql", query: query, variables: variables) conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error1, error2]} = json_response(conn, 200) %{
assert error1["message"] =~ ~s(Field addresses is too complex) "data" => %{
assert error2["message"] =~ ~s(Operation is too complex) "address" => %{
"transactions" => %{
"edges" => transactions
}
}
}
} = json_response(conn, 200)
assert length(transactions) == 3
assert Enum.all?(transactions, &(&1["node"]["block_number"] == first_block.number))
end
test "pagination support with 'first' and 'after' arguments", %{conn: conn} do
first_block = insert(:block)
second_block = insert(:block)
third_block = insert(:block)
address = insert(:address)
3
|> insert_list(:transaction, from_address: address)
|> with_block(second_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(third_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(first_block)
query1 = """
query ($hash: AddressHash!, $first: Int!) {
address(hash: $hash) {
transactions(first: $first) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
hash
block_number
}
cursor
}
}
}
}
"""
variables1 = %{
"hash" => to_string(address.hash),
"first" => 3
}
conn = get(conn, "/graphql", query: query1, variables: variables1)
%{"data" => %{"address" => %{"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"]["block_number"] == third_block.number))
last_cursor_page1 =
page1
|> Map.get("edges")
|> List.last()
|> Map.get("cursor")
query2 = """
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
}
}
}
}
"""
variables2 = %{
"hash" => to_string(address.hash),
"first" => 3,
"after" => last_cursor_page1
}
conn = get(conn, "/graphql", query: query2, variables: variables2)
%{"data" => %{"address" => %{"transactions" => page2}}} = json_response(conn, 200)
assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true}
assert Enum.all?(page2["edges"], &(&1["node"]["block_number"] == second_block.number))
last_cursor_page2 =
page2
|> Map.get("edges")
|> List.last()
|> 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 = %{
"hash" => to_string(address.hash),
"first" => 3,
"after" => last_cursor_page2
}
conn = get(conn, "/graphql", query: query3, variables: variables3)
%{"data" => %{"address" => %{"transactions" => page3}}} = json_response(conn, 200)
assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true}
assert Enum.all?(page3["edges"], &(&1["node"]["block_number"] == first_block.number))
end end
end end
end end

@ -0,0 +1,185 @@
defmodule BlockScoutWeb.Schema.Query.AddressesTest do
use BlockScoutWeb.ConnCase
describe "addresses field" do
test "with valid argument 'hashes', returns all expected fields", %{conn: conn} do
address = insert(:address, fetched_coin_balance: 100)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
hash
fetched_coin_balance
fetched_coin_balance_block_number
contract_code
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"hash" => to_string(address.hash),
"fetched_coin_balance" => to_string(address.fetched_coin_balance.value),
"fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number,
"contract_code" => nil
}
]
}
}
end
test "with contract address, `contract_code` is serialized as expected", %{conn: conn} do
address = insert(:contract_address, fetched_coin_balance: 100)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
contract_code
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"contract_code" => to_string(address.contract_code)
}
]
}
}
end
test "smart_contract returns all expected fields", %{conn: conn} do
address = insert(:address, fetched_coin_balance: 100)
smart_contract = insert(:smart_contract, address_hash: address.hash)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
smart_contract {
name
compiler_version
optimization
contract_source_code
abi
address_hash
}
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"fetched_coin_balance" => to_string(address.fetched_coin_balance.value),
"smart_contract" => %{
"name" => smart_contract.name,
"compiler_version" => smart_contract.compiler_version,
"optimization" => smart_contract.optimization,
"contract_source_code" => smart_contract.contract_source_code,
"abi" => Jason.encode!(smart_contract.abi),
"address_hash" => to_string(address.hash)
}
}
]
}
}
end
test "errors for non-existent address hashes", %{conn: conn} do
address = build(:address)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
}
}
"""
variables = %{"hashes" => [to_string(address.hash)]}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Addresses not found.)
end
test "errors if argument 'hashes' is missing", %{conn: conn} do
query = """
query {
addresses {
fetched_coin_balance
}
}
"""
variables = %{}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == ~s(In argument "hashes": Expected type "[AddressHash!]!", found null.)
end
test "errors if argument 'hashes' is not a list of address hashes", %{conn: conn} do
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
}
}
"""
variables = %{"hashes" => ["someInvalidHash"]}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Argument "hashes" has invalid value)
end
test "correlates complexity to size of 'hashes' argument", %{conn: conn} do
# max of 50 addresses with four fields of complexity 1 can be fetched
# per query:
# 50 * 4 = 200, which is equal to a max complexity of 200
hashes = 51 |> build_list(:address) |> Enum.map(&to_string(&1.hash))
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
hash
fetched_coin_balance
fetched_coin_balance_block_number
contract_code
}
}
"""
variables = %{"hashes" => hashes}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error1, error2]} = json_response(conn, 200)
assert error1["message"] =~ ~s(Field addresses is too complex)
assert error2["message"] =~ ~s(Operation is too complex)
end
end
end

@ -0,0 +1,35 @@
defmodule BlockScoutWeb.Schema.Query.NodeTest do
use BlockScoutWeb.ConnCase
describe "node field" do
test "with valid argument 'id' for a transaction", %{conn: conn} do
transaction = insert(: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)
assert json_response(conn, 200) == %{
"data" => %{
"node" => %{
"id" => id,
"hash" => to_string(transaction.hash)
}
}
}
end
end
end

@ -74,7 +74,7 @@ defmodule EthereumJSONRPC.Parity do
defp extract_beneficiaries(traces) when is_list(traces) do defp extract_beneficiaries(traces) when is_list(traces) do
Enum.reduce(traces, MapSet.new(), fn Enum.reduce(traces, MapSet.new(), fn
%{"action" => %{"rewardType" => "block", "author" => author}, "blockNumber" => block_number}, beneficiaries -> %{"type" => "reward", "blockNumber" => block_number, "action" => %{"author" => author}}, beneficiaries ->
beneficiary = %{ beneficiary = %{
block_number: block_number, block_number: block_number,
address_hash: author address_hash: author

@ -294,6 +294,64 @@ defmodule EthereumJSONRPC.ParityTest do
assert fetched_beneficiaries == expected_beneficiaries assert fetched_beneficiaries == expected_beneficiaries
end end
test "with 'external' 'rewardType'", %{
json_rpc_named_arguments: json_rpc_named_arguments
} do
block_number = 5_609_295
block_quantity = EthereumJSONRPC.integer_to_quantity(block_number)
hash1 = "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
hash2 = "0x523b6539ff08d72a6c8bb598af95bf50c1ea839c"
if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{params: [^block_quantity]}, _options ->
{:ok,
[
%{
"action" => %{
"author" => hash1,
"rewardType" => "external",
"value" => "0xde0b6b3a7640000"
},
"blockHash" => "0xf19a4ea2bb4f2d8839f4c3ec11e0e86c29d57799d7073713958fe1990e197cf5",
"blockNumber" => 5_609_295,
"result" => nil,
"subtraces" => 0,
"traceAddress" => [],
"transactionHash" => nil,
"transactionPosition" => nil,
"type" => "reward"
},
%{
"action" => %{
"author" => hash2,
"rewardType" => "external",
"value" => "0xde0b6b3a7640000"
},
"blockHash" => "0xf19a4ea2bb4f2d8839f4c3ec11e0e86c29d57799d7073713958fe1990e197cf5",
"blockNumber" => 5_609_295,
"result" => nil,
"subtraces" => 0,
"traceAddress" => [],
"transactionHash" => nil,
"transactionPosition" => nil,
"type" => "reward"
}
]}
end)
end
expected_beneficiaries =
MapSet.new([
%{block_number: block_number, address_hash: hash2},
%{block_number: block_number, address_hash: hash1}
])
{:ok, fetched_beneficiaries} =
EthereumJSONRPC.Parity.fetch_beneficiaries(5_609_295..5_609_295, json_rpc_named_arguments)
assert fetched_beneficiaries == expected_beneficiaries
end
test "with no rewards, returns {:ok, []}", %{ test "with no rewards, returns {:ok, []}", %{
json_rpc_named_arguments: json_rpc_named_arguments json_rpc_named_arguments: json_rpc_named_arguments
} do } do
@ -309,7 +367,7 @@ defmodule EthereumJSONRPC.ParityTest do
end end
end end
test "with nil rewards, returns {:error, }", %{ test "with nil rewards, returns {:error, reason}", %{
json_rpc_named_arguments: json_rpc_named_arguments json_rpc_named_arguments: json_rpc_named_arguments
} do } do
if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do

@ -0,0 +1,22 @@
defmodule Explorer.GraphQL do
@moduledoc """
The GraphQL context.
"""
import Ecto.Query,
only: [
order_by: 3,
or_where: 3,
where: 3
]
alias Explorer.Chain.{Address, Hash, Transaction}
def address_to_transactions_query(%Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}) do
Transaction
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
|> where([transaction], transaction.to_address_hash == ^address_hash)
|> or_where([transaction], transaction.from_address_hash == ^address_hash)
|> or_where([transaction], transaction.created_contract_address_hash == ^address_hash)
end
end

@ -0,0 +1,90 @@
defmodule Explorer.GraphQLTest do
use Explorer.DataCase
import Explorer.Factory
alias Explorer.{GraphQL, Repo}
describe "address_to_transactions_query/1" do
test "with address hash with zero transactions" do
result =
:address
|> insert()
|> GraphQL.address_to_transactions_query()
|> Repo.all()
assert result == []
end
test "with matching 'to_address_hash'" do
address = insert(:address)
transaction = insert(:transaction, to_address: address)
insert(:transaction)
[found_transaction] =
address
|> GraphQL.address_to_transactions_query()
|> Repo.all()
assert found_transaction.hash == transaction.hash
end
test "with matching 'from_address_hash'" do
address = insert(:address)
transaction = insert(:transaction, from_address: address)
insert(:transaction)
[found_transaction] =
address
|> GraphQL.address_to_transactions_query()
|> Repo.all()
assert found_transaction.hash == transaction.hash
end
test "with matching 'created_contract_address_hash'" do
address = insert(:address)
transaction = insert(:transaction, created_contract_address: address)
insert(:transaction)
[found_transaction] =
address
|> GraphQL.address_to_transactions_query()
|> Repo.all()
assert found_transaction.hash == transaction.hash
end
test "orders by descending block and index" do
first_block = insert(:block)
second_block = insert(:block)
third_block = insert(:block)
address = insert(:address)
3
|> insert_list(:transaction, from_address: address)
|> with_block(second_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(third_block)
3
|> insert_list(:transaction, from_address: address)
|> with_block(first_block)
found_transactions =
address
|> GraphQL.address_to_transactions_query()
|> Repo.all()
block_number_and_index_order =
Enum.map(found_transactions, fn transaction ->
{transaction.block_number, transaction.index}
end)
assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2))
end
end
end

@ -4,6 +4,7 @@
"absinthe": {:hex, :absinthe, "1.4.13", "81eb2ff41f1b62cd6e992955f62c22c042d1079b7936c27f5f7c2c806b8fc436", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "absinthe": {:hex, :absinthe, "1.4.13", "81eb2ff41f1b62cd6e992955f62c22c042d1079b7936c27f5f7c2c806b8fc436", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.3", "cea34e7ebbc9a252038c1f1164878ee86bcb108905fe462be77efacda15c1e70", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10.5 or ~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.3", "cea34e7ebbc9a252038c1f1164878ee86bcb108905fe462be77efacda15c1e70", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10.5 or ~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.6", "ac5d2d3d02acf52fda0f151b294017ab06e2ed1c6c15334e06aac82c94e36e08", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "absinthe_plug": {:hex, :absinthe_plug, "1.4.6", "ac5d2d3d02acf52fda0f151b294017ab06e2ed1c6c15334e06aac82c94e36e08", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_relay": {:hex, :absinthe_relay, "1.4.4", "d0a6d8e71375a6026974d227456c8a73ea8eea7c7b00e698603ab5a96066c333", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"accept": {:hex, :accept, "0.3.3", "548ebb6fb2e8b0d170e75bb6123aea6ceecb0189bb1231eeadf52eac08384a97", [:rebar3], [], "hexpm"}, "accept": {:hex, :accept, "0.3.3", "548ebb6fb2e8b0d170e75bb6123aea6ceecb0189bb1231eeadf52eac08384a97", [:rebar3], [], "hexpm"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"benchee": {:hex, :benchee, "0.13.2", "30cd4ff5f593fdd218a9b26f3c24d580274f297d88ad43383afe525b1543b165", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, "benchee": {:hex, :benchee, "0.13.2", "30cd4ff5f593fdd218a9b26f3c24d580274f297d88ad43383afe525b1543b165", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},

Loading…
Cancel
Save