feat: add verbosity to GraphQL token transfers query (#10770)
* feat: GraphQL query for token transfers with verbose token, transaction, and block info * feat: extend GraphQL schema to support legacy `tokenTransferTxs` query * fix: cspell warnings * chore: add spec and doc for `token_transfers_by_address_hash/4` * chore: remove duplicate function declaration * fix: do not treat examples as doctestspull/10844/head
parent
2e6ce2c222
commit
e66d345b96
@ -0,0 +1,22 @@ |
|||||||
|
defmodule BlockScoutWeb.GraphQL.Celo.Resolvers.TokenTransfer do |
||||||
|
@moduledoc """ |
||||||
|
Resolvers for token transfers, used in the CELO schema. |
||||||
|
""" |
||||||
|
|
||||||
|
alias Absinthe.Relay.Connection |
||||||
|
alias Explorer.GraphQL.Celo, as: GraphQL |
||||||
|
alias Explorer.Repo |
||||||
|
|
||||||
|
def get_by(_, args, _) do |
||||||
|
connection_args = Map.take(args, [:after, :before, :first, :last]) |
||||||
|
|
||||||
|
GraphQL.token_tx_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 |
@ -0,0 +1,34 @@ |
|||||||
|
defmodule BlockScoutWeb.GraphQL.Celo.Resolvers.TokenTransferTx do |
||||||
|
@moduledoc false |
||||||
|
|
||||||
|
alias Absinthe.Relay.Connection |
||||||
|
alias Explorer.GraphQL.Celo, as: GraphQL |
||||||
|
alias Explorer.Repo |
||||||
|
|
||||||
|
def get_by(_, %{address_hash: address_hash, first: limit} = args, _) do |
||||||
|
connection_args = Map.take(args, [:after, :before, :first, :last]) |
||||||
|
|
||||||
|
offset = |
||||||
|
case Connection.offset(args) do |
||||||
|
{:ok, offset} when is_integer(offset) -> offset |
||||||
|
_ -> 0 |
||||||
|
end |
||||||
|
|
||||||
|
address_hash |
||||||
|
|> GraphQL.token_tx_transfers_query_for_address(offset, limit) |
||||||
|
|> Connection.from_query(&Repo.all/1, connection_args, options(args)) |
||||||
|
end |
||||||
|
|
||||||
|
def get_by(_, args, _) do |
||||||
|
connection_args = Map.take(args, [:after, :before, :first, :last]) |
||||||
|
|
||||||
|
GraphQL.token_tx_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 |
@ -0,0 +1,28 @@ |
|||||||
|
defmodule BlockScoutWeb.GraphQL.Celo.QueryFields do |
||||||
|
@moduledoc """ |
||||||
|
Query fields for the CELO schema. |
||||||
|
""" |
||||||
|
|
||||||
|
alias BlockScoutWeb.GraphQL.Celo.Resolvers.TokenTransferTx |
||||||
|
|
||||||
|
use Absinthe.Schema.Notation |
||||||
|
use Absinthe.Relay.Schema, :modern |
||||||
|
|
||||||
|
defmacro generate do |
||||||
|
quote do |
||||||
|
@desc "Gets token transfer transactions." |
||||||
|
connection field(:token_transfer_txs, node_type: :transfer_tx) do |
||||||
|
arg(:address_hash, :address_hash) |
||||||
|
arg(:count, :integer) |
||||||
|
|
||||||
|
resolve(&TokenTransferTx.get_by/3) |
||||||
|
|
||||||
|
complexity(fn |
||||||
|
%{first: first}, child_complexity -> first * child_complexity |
||||||
|
%{last: last}, child_complexity -> last * child_complexity |
||||||
|
%{}, _child_complexity -> 0 |
||||||
|
end) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,87 @@ |
|||||||
|
defmodule BlockScoutWeb.GraphQL.Celo.Schema.Types do |
||||||
|
@moduledoc false |
||||||
|
|
||||||
|
use Absinthe.Schema.Notation |
||||||
|
use Absinthe.Relay.Schema.Notation, :modern |
||||||
|
|
||||||
|
alias BlockScoutWeb.GraphQL.Celo.Resolvers.TokenTransfer |
||||||
|
|
||||||
|
@desc """ |
||||||
|
Represents a CELO or usd token transfer between addresses. |
||||||
|
""" |
||||||
|
node object(:celo_transfer, id_fetcher: &celo_transfer_id_fetcher/2) do |
||||||
|
field(:value, :decimal) |
||||||
|
field(:token, :string) |
||||||
|
field(:token_address, :string) |
||||||
|
field(:token_type, :string) |
||||||
|
field(:token_id, :decimal) |
||||||
|
field(:block_number, :integer) |
||||||
|
field(:from_address_hash, :address_hash) |
||||||
|
field(:to_address_hash, :address_hash) |
||||||
|
field(:transaction_hash, :full_hash) |
||||||
|
|
||||||
|
field(:log_index, :integer) |
||||||
|
|
||||||
|
field(:gas_price, :wei) |
||||||
|
field(:gas_used, :decimal) |
||||||
|
field(:input, :string) |
||||||
|
field(:timestamp, :datetime) |
||||||
|
field(:comment, :string) |
||||||
|
|
||||||
|
field(:to_account_hash, :address_hash) |
||||||
|
field(:from_account_hash, :address_hash) |
||||||
|
end |
||||||
|
|
||||||
|
@desc """ |
||||||
|
Represents a CELO token transfer between addresses. |
||||||
|
""" |
||||||
|
node object(:transfer_tx, id_fetcher: &transfer_tx_id_fetcher/2) do |
||||||
|
field(:gateway_fee_recipient, :address_hash) |
||||||
|
field(:gateway_fee, :address_hash) |
||||||
|
field(:fee_currency, :address_hash) |
||||||
|
field(:fee_token, :string) |
||||||
|
field(:address_hash, :address_hash) |
||||||
|
field(:transaction_hash, :full_hash) |
||||||
|
field(:block_number, :integer) |
||||||
|
field(:gas_price, :wei) |
||||||
|
field(:gas_used, :decimal) |
||||||
|
field(:input, :string) |
||||||
|
field(:timestamp, :datetime) |
||||||
|
|
||||||
|
connection field(:token_transfer, node_type: :celo_transfer) do |
||||||
|
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 |
||||||
|
end |
||||||
|
|
||||||
|
connection(node_type: :transfer_tx) |
||||||
|
connection(node_type: :celo_transfer) |
||||||
|
|
||||||
|
defp transfer_tx_id_fetcher( |
||||||
|
%{transaction_hash: transaction_hash, address_hash: address_hash}, |
||||||
|
_ |
||||||
|
) do |
||||||
|
Jason.encode!(%{ |
||||||
|
transaction_hash: to_string(transaction_hash), |
||||||
|
address_hash: to_string(address_hash) |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
defp celo_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 |
||||||
|
end |
@ -0,0 +1,19 @@ |
|||||||
|
defmodule BlockScoutWeb.GraphQL.Resolvers.Token do |
||||||
|
@moduledoc false |
||||||
|
|
||||||
|
alias BlockScoutWeb.GraphQL.Resolvers.Helper |
||||||
|
alias Explorer.Chain.TokenTransfer |
||||||
|
alias Explorer.GraphQL |
||||||
|
|
||||||
|
def get_by( |
||||||
|
%TokenTransfer{token_contract_address_hash: token_contract_address_hash}, |
||||||
|
_, |
||||||
|
resolution |
||||||
|
) do |
||||||
|
if resolution.context.api_enabled do |
||||||
|
GraphQL.get_token(%{contract_address_hash: token_contract_address_hash}) |
||||||
|
else |
||||||
|
{:error, Helper.api_is_disabled()} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,139 @@ |
|||||||
|
defmodule Explorer.GraphQL.Celo do |
||||||
|
@moduledoc """ |
||||||
|
Defines Ecto queries to fetch Celo blockchain data for the legacy GraphQL |
||||||
|
schema. |
||||||
|
|
||||||
|
Includes functions to construct queries for token transfers and transactions. |
||||||
|
""" |
||||||
|
|
||||||
|
import Ecto.Query, |
||||||
|
only: [from: 2, order_by: 3, where: 3, subquery: 1] |
||||||
|
|
||||||
|
alias Explorer.Chain.{ |
||||||
|
Block, |
||||||
|
Hash, |
||||||
|
Token, |
||||||
|
TokenTransfer, |
||||||
|
Transaction |
||||||
|
} |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Constructs a paginated query for token transfers involving a specific address. |
||||||
|
""" |
||||||
|
@spec token_tx_transfers_query_for_address(Hash.Address.t(), integer(), integer()) :: Ecto.Query.t() |
||||||
|
def token_tx_transfers_query_for_address(address_hash, offset, limit) do |
||||||
|
page = floor(offset / limit) + 1 |
||||||
|
growing_limit = limit * (page + 1) |
||||||
|
|
||||||
|
tokens = |
||||||
|
from( |
||||||
|
tt in TokenTransfer, |
||||||
|
where: not is_nil(tt.transaction_hash), |
||||||
|
where: tt.to_address_hash == ^address_hash, |
||||||
|
or_where: tt.from_address_hash == ^address_hash, |
||||||
|
select: %{ |
||||||
|
transaction_hash: tt.transaction_hash, |
||||||
|
block_number: tt.block_number, |
||||||
|
to_address_hash: tt.to_address_hash, |
||||||
|
from_address_hash: tt.from_address_hash |
||||||
|
}, |
||||||
|
distinct: [desc: tt.block_number, desc: tt.transaction_hash], |
||||||
|
order_by: [ |
||||||
|
desc: tt.block_number, |
||||||
|
desc: tt.transaction_hash, |
||||||
|
desc: tt.from_address_hash, |
||||||
|
desc: tt.to_address_hash |
||||||
|
], |
||||||
|
limit: ^growing_limit |
||||||
|
) |
||||||
|
|
||||||
|
query = |
||||||
|
from( |
||||||
|
tt in subquery(tokens), |
||||||
|
as: :token_transfer, |
||||||
|
inner_join: tx in Transaction, |
||||||
|
as: :transaction, |
||||||
|
on: tx.hash == tt.transaction_hash, |
||||||
|
inner_join: b in Block, |
||||||
|
on: tx.block_hash == b.hash, |
||||||
|
left_join: token in Token, |
||||||
|
on: tx.gas_token_contract_address_hash == token.contract_address_hash, |
||||||
|
select: %{ |
||||||
|
transaction_hash: tt.transaction_hash, |
||||||
|
to_address_hash: tt.to_address_hash, |
||||||
|
from_address_hash: tt.from_address_hash, |
||||||
|
gas_used: tx.gas_used, |
||||||
|
gas_price: tx.gas_price, |
||||||
|
fee_currency: tx.gas_token_contract_address_hash, |
||||||
|
fee_token: fragment("coalesce(?, 'CELO')", token.symbol), |
||||||
|
# gateway_fee: tx.gateway_fee, |
||||||
|
# gateway_fee_recipient: tx.gas_fee_recipient_hash, |
||||||
|
timestamp: b.timestamp, |
||||||
|
input: tx.input, |
||||||
|
nonce: tx.nonce, |
||||||
|
block_number: tt.block_number |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
query |
||||||
|
|> order_by([transaction: t], |
||||||
|
desc: t.block_number, |
||||||
|
desc: t.hash, |
||||||
|
asc: t.nonce, |
||||||
|
desc: t.from_address_hash, |
||||||
|
desc: t.to_address_hash |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Constructs a query for token transfers filtered by a specific address. |
||||||
|
""" |
||||||
|
@spec token_tx_transfers_query_by_address(Hash.Address.t()) :: Ecto.Query.t() |
||||||
|
def token_tx_transfers_query_by_address(address_hash) do |
||||||
|
token_tx_transfers_query() |
||||||
|
|> where([t], t.from_address_hash == ^address_hash or t.to_address_hash == ^address_hash) |
||||||
|
|> order_by([transaction: t], desc: t.block_number, asc: t.nonce) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Constructs a query to fetch detailed token transfer information. |
||||||
|
""" |
||||||
|
@spec token_tx_transfers_query() :: Ecto.Query.t() |
||||||
|
def token_tx_transfers_query do |
||||||
|
from( |
||||||
|
tt in TokenTransfer, |
||||||
|
inner_join: tx in Transaction, |
||||||
|
as: :transaction, |
||||||
|
on: tx.hash == tt.transaction_hash, |
||||||
|
inner_join: b in Block, |
||||||
|
on: tt.block_number == b.number, |
||||||
|
# left_join: wf in CeloWalletAccounts, |
||||||
|
# on: tt.from_address_hash == wf.wallet_address_hash, |
||||||
|
# left_join: wt in CeloWalletAccounts, |
||||||
|
# on: tt.to_address_hash == wt.wallet_address_hash, |
||||||
|
left_join: token in Token, |
||||||
|
on: tt.token_contract_address_hash == token.contract_address_hash, |
||||||
|
select: %{ |
||||||
|
gas_used: tx.gas_used, |
||||||
|
gas_price: tx.gas_price, |
||||||
|
timestamp: b.timestamp, |
||||||
|
input: tx.input, |
||||||
|
transaction_hash: tt.transaction_hash, |
||||||
|
from_address_hash: tt.from_address_hash, |
||||||
|
to_address_hash: tt.to_address_hash, |
||||||
|
# from_account_hash: wf.account_address_hash, |
||||||
|
# to_account_hash: wt.account_address_hash, |
||||||
|
log_index: tt.log_index, |
||||||
|
value: tt.amount, |
||||||
|
# comment: tt.comment, |
||||||
|
token: token.symbol, |
||||||
|
token_address: token.contract_address_hash, |
||||||
|
nonce: tx.nonce, |
||||||
|
block_number: tt.block_number, |
||||||
|
token_type: token.type, |
||||||
|
token_id: fragment("(COALESCE(?, ARRAY[]::Decimal[]))[1]", tt.token_ids) |
||||||
|
}, |
||||||
|
order_by: [desc: tt.block_number, desc: tt.amount, desc: tt.log_index] |
||||||
|
) |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue