GraphQL API query to get addresses by hash

Why:

* We'd like to support GraphQL API queries for getting single or
multiple addresses by hashes. API users could use this instead of the
`balance` and `balancemulti` actions on the RPC API.

  Sample document:
  ```
   query ($hashes: [AddressHash!]!) {
     addresses(hashes: $hashes) {
       hash
       fetched_coin_balance
       fetched_coin_balance_block_number
       contract_code
     }
   }
  ```
* Issue link: n/a

This change addresses the need by:

* Creating `BlockScoutWeb.Resolvers.Address` with a single resolver
function that gets addresses by a list of hashes.
* Adding `:data` scalar to `BlockScoutWeb.Schema.Scalars`.
* Adding `address` object type to `BlockScoutWeb.Schema.Types`. Uses new
`:data` scalar mentioned above.
* Adding `addresses` field to query in `BlockScoutWeb.Schema`. Uses the
new `address` object type and the resolver function mentioned above.
* Editing `Abinthe.Plug` and `GraphiQL` in router to analyze complexity
and set `max_complexity` to 50.
pull/1031/head
Sebastian Abondano 6 years ago
parent 2d7b37db6f
commit 67c68b6f41
  1. 12
      apps/block_scout_web/lib/block_scout_web/resolvers/address.ex
  2. 10
      apps/block_scout_web/lib/block_scout_web/router.ex
  3. 9
      apps/block_scout_web/lib/block_scout_web/schema.ex
  4. 19
      apps/block_scout_web/lib/block_scout_web/schema/scalars.ex
  5. 10
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  6. 142
      apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs

@ -0,0 +1,12 @@
defmodule BlockScoutWeb.Resolvers.Address do
@moduledoc false
alias Explorer.Chain
def get_by(_, %{hashes: hashes}, _) do
case Chain.hashes_to_addresses(hashes) do
[] -> {:error, "Addresses not found."}
result -> {:ok, result}
end
end
end

@ -38,12 +38,18 @@ defmodule BlockScoutWeb.Router do
}) })
end end
forward("/graphql", Absinthe.Plug, schema: BlockScoutWeb.Schema) forward("/graphql", Absinthe.Plug,
schema: BlockScoutWeb.Schema,
analyze_complexity: true,
max_complexity: 50
)
forward("/graphiql", Absinthe.Plug.GraphiQL, forward("/graphiql", Absinthe.Plug.GraphiQL,
schema: BlockScoutWeb.Schema, schema: BlockScoutWeb.Schema,
interface: :playground, interface: :playground,
socket: BlockScoutWeb.UserSocket socket: BlockScoutWeb.UserSocket,
analyze_complexity: true,
max_complexity: 50
) )
scope "/", BlockScoutWeb do scope "/", BlockScoutWeb do

@ -3,11 +3,18 @@ defmodule BlockScoutWeb.Schema do
use Absinthe.Schema use Absinthe.Schema
alias BlockScoutWeb.Resolvers.{Block, Transaction} alias BlockScoutWeb.Resolvers.{Address, Block, Transaction}
import_types(BlockScoutWeb.Schema.Types) import_types(BlockScoutWeb.Schema.Types)
query do query do
@desc "Gets addresses by address hash."
field :addresses, list_of(:address) do
arg(:hashes, non_null(list_of(non_null(:address_hash))))
resolve(&Address.get_by/3)
complexity(fn %{hashes: hashes}, child_complexity -> length(hashes) * child_complexity end)
end
@desc "Gets a block by number." @desc "Gets a block by number."
field :block, :block do field :block, :block do
arg(:number, non_null(:integer)) arg(:number, non_null(:integer))

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Schema.Scalars do
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
alias Explorer.Chain.{Hash, Wei} alias Explorer.Chain.{Data, Hash, Wei}
alias Explorer.Chain.Hash.{Address, Full, Nonce} alias Explorer.Chain.Hash.{Address, Full, Nonce}
@desc """ @desc """
@ -24,6 +24,23 @@ defmodule BlockScoutWeb.Schema.Scalars do
serialize(&to_string/1) serialize(&to_string/1)
end end
@desc """
An unpadded hexadecimal number with 0 or more digits. Each pair of digits
maps directly to a byte in the underlying binary representation. When
interpreted as a number, it should be treated as big-endian.
"""
scalar :data do
parse(fn
%Absinthe.Blueprint.Input.String{value: value} ->
Data.cast(value)
_ ->
:error
end)
serialize(&to_string/1)
end
@desc """ @desc """
A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash. A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash.
""" """

@ -6,6 +6,16 @@ defmodule BlockScoutWeb.Schema.Types do
import_types(Absinthe.Type.Custom) import_types(Absinthe.Type.Custom)
import_types(BlockScoutWeb.Schema.Scalars) import_types(BlockScoutWeb.Schema.Scalars)
@desc """
A stored representation of a Web3 address.
"""
object :address do
field(:hash, :address_hash)
field(:fetched_coin_balance, :wei)
field(:fetched_coin_balance_block_number, :integer)
field(:contract_code, :data)
end
@desc """ @desc """
A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally
other data. Because each block (except for the initial "genesis block") points to the previous block, the data other data. Because each block (except for the initial "genesis block") points to the previous block, the data

@ -0,0 +1,142 @@
defmodule BlockScoutWeb.Schema.Query.AddressTest do
use BlockScoutWeb.ConnCase
describe "address 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 "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 12 addresses with four fields of complexity 1 can be fetched
# per query:
# 12 * 4 = 48, which is less than a max complexity of 50
hashes = 13 |> 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
Loading…
Cancel
Save