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