From e66d345b96a2d3b2cdece4ad5fbd996cef44e099 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 27 Sep 2024 17:17:05 +0300 Subject: [PATCH] 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 doctests --- .../graphql/celo/resolvers/token_transfer.ex | 22 +++ .../celo/resolvers/token_transfer_tx.ex | 34 +++++ .../graphql/celo/schema/query_fields.ex | 28 ++++ .../graphql/celo/schema/types.ex | 87 +++++++++++ .../graphql/resolvers/block.ex | 15 +- .../graphql/resolvers/token.ex | 19 +++ .../graphql/resolvers/token_transfer.ex | 15 +- .../graphql/resolvers/transaction.ex | 18 ++- .../lib/block_scout_web/graphql/schema.ex | 11 ++ .../block_scout_web/graphql/schema/types.ex | 135 ++++++++++++----- apps/explorer/lib/explorer/chain.ex | 4 +- .../lib/explorer/chain/token_transfer.ex | 43 +++++- apps/explorer/lib/explorer/graphql.ex | 13 ++ apps/explorer/lib/explorer/graphql/celo.ex | 139 ++++++++++++++++++ 14 files changed, 538 insertions(+), 45 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer_tx.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/query_fields.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/types.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token.ex create mode 100644 apps/explorer/lib/explorer/graphql/celo.ex diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer.ex new file mode 100644 index 0000000000..31e4a252c3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer_tx.ex b/apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer_tx.ex new file mode 100644 index 0000000000..557b30dde0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/graphql/celo/resolvers/token_transfer_tx.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/query_fields.ex b/apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/query_fields.ex new file mode 100644 index 0000000000..f24fa7b635 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/query_fields.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/types.ex new file mode 100644 index 0000000000..6d65f185f0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/graphql/celo/schema/types.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/block.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/block.ex index 913ae80586..24de1e6259 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/block.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/block.ex @@ -3,14 +3,27 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.Block do alias BlockScoutWeb.GraphQL.Resolvers.Helper alias Explorer.Chain + alias Explorer.Chain.Transaction + + @api_true [api?: true] def get_by(_, %{number: number}, resolution) do with {:api_enabled, true} <- {:api_enabled, resolution.context.api_enabled}, - {:ok, _} = result <- Chain.number_to_block(number) do + {:ok, _} = result <- Chain.number_to_block(number, @api_true) do result else {:api_enabled, false} -> {:error, Helper.api_is_disabled()} {:error, :not_found} -> {:error, "Block number #{number} was not found."} end end + + def get_by(%Transaction{block_hash: hash}, _, resolution) do + with {:api_enabled, true} <- {:api_enabled, resolution.context.api_enabled}, + {:ok, _} = result <- Chain.hash_to_block(hash, @api_true) do + result + else + {:api_enabled, false} -> {:error, Helper.api_is_disabled()} + {:error, :not_found} -> {:error, "Block hash #{to_string(hash)} was not found."} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token.ex new file mode 100644 index 0000000000..a830537b38 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex index 2fe2a05a65..2302befd71 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.TokenTransfer do alias Absinthe.Relay.Connection alias BlockScoutWeb.GraphQL.Resolvers.Helper + alias Explorer.Chain.{Address, TokenTransfer} alias Explorer.{GraphQL, Repo} def get_by(%{transaction_hash: _, log_index: _} = args, resolution) do @@ -19,7 +20,19 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.TokenTransfer do token_contract_address_hash |> GraphQL.list_token_transfers_query() - |> Connection.from_query(&Repo.all/1, connection_args, options(args)) + |> Connection.from_query(&Repo.replica().all/1, connection_args, options(args)) + else + {:error, Helper.api_is_disabled()} + end + end + + def get_by(%Address{hash: address_hash}, args, resolution) do + if resolution.context.api_enabled do + connection_args = Map.take(args, [:after, :before, :first, :last]) + + address_hash + |> TokenTransfer.token_transfers_by_address_hash(nil, [], nil) + |> Connection.from_query(&Repo.replica().all/1, connection_args, options(args)) else {:error, Helper.api_is_disabled()} end diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex index 2dd299abae..3dbeb9924f 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex @@ -4,11 +4,13 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.Transaction do alias Absinthe.Relay.Connection alias BlockScoutWeb.GraphQL.Resolvers.Helper alias Explorer.{Chain, GraphQL, Repo} - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, TokenTransfer} + + @api_true [api?: true] def get_by(_, %{hash: hash}, resolution) do with {:api_enabled, true} <- {:api_enabled, resolution.context.api_enabled}, - {:ok, transaction} <- Chain.hash_to_transaction(hash) do + {:ok, transaction} <- Chain.hash_to_transaction(hash, @api_true) do {:ok, transaction} else {:api_enabled, false} -> {:error, Helper.api_is_disabled()} @@ -22,12 +24,22 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.Transaction do if resolution.context.api_enabled do address_hash |> GraphQL.address_to_transactions_query(args.order) - |> Connection.from_query(&Repo.all/1, connection_args, options(args)) + |> Connection.from_query(&Repo.replica().all/1, connection_args, options(args)) else {:error, Helper.api_is_disabled()} end end + def get_by(%TokenTransfer{transaction_hash: hash}, _, resolution) do + with {:api_enabled, true} <- {:api_enabled, resolution.context.api_enabled}, + {:ok, transaction} <- Chain.hash_to_transaction(hash, @api_true) do + {:ok, transaction} + else + {:api_enabled, false} -> {:error, Helper.api_is_disabled()} + {:error, :not_found} -> {:error, "Transaction not found."} + end + end + defp options(%{before: _}), do: [] defp options(%{count: count}), do: [count: count] diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex b/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex index 91b11a6959..1bfee0f8a4 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex @@ -22,6 +22,10 @@ defmodule BlockScoutWeb.GraphQL.Schema do import_types(BlockScoutWeb.GraphQL.Schema.Types) + if Application.compile_env(:explorer, :chain_type) == :celo do + import_types(BlockScoutWeb.GraphQL.Celo.Schema.Types) + end + node interface do resolve_type(fn %ExplorerChainInternalTransaction{}, _ -> @@ -100,6 +104,13 @@ defmodule BlockScoutWeb.GraphQL.Schema do arg(:hash, non_null(:full_hash)) resolve(&Transaction.get_by/3) end + + if Application.compile_env(:explorer, :chain_type) == :celo do + require BlockScoutWeb.GraphQL.Celo.QueryFields + alias BlockScoutWeb.GraphQL.Celo.QueryFields + + QueryFields.generate() + end end subscription do diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex index 192d38d2f1..6cfde913b4 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex @@ -1,3 +1,66 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Transaction do + @moduledoc false + + alias BlockScoutWeb.GraphQL.Resolvers.{Block, InternalTransaction} + + case Application.compile_env(:explorer, :chain_type) do + :celo -> + @chain_type_fields quote( + do: [ + field(:gas_token_contract_address_hash, :address_hash) + ] + ) + + _ -> + @chain_type_fields quote(do: []) + end + + defmacro generate do + quote do + node object(:transaction, id_fetcher: &transaction_id_fetcher/2) do + field(:cumulative_gas_used, :decimal) + field(:error, :string) + field(:gas, :decimal) + field(:gas_price, :wei) + field(:gas_used, :decimal) + field(:hash, :full_hash) + field(:index, :integer) + field(:input, :string) + field(:nonce, :nonce_hash) + field(:r, :decimal) + field(:s, :decimal) + field(:status, :status) + field(:v, :decimal) + field(:value, :wei) + field(:block_hash, :full_hash) + field(:block_number, :integer) + field(:from_address_hash, :address_hash) + field(:to_address_hash, :address_hash) + field(:created_contract_address_hash, :address_hash) + field(:earliest_processing_start, :datetime) + field(:revert_reason, :string) + field(:max_priority_fee_per_gas, :wei) + field(:max_fee_per_gas, :wei) + field(:type, :integer) + field(:has_error_in_internal_txs, :boolean) + + field :block, :block do + resolve(&Block.get_by/3) + end + + connection field(:internal_transactions, node_type: :internal_transaction) do + arg(:count, :integer) + resolve(&InternalTransaction.get_by/3) + + complexity(fn params, child_complexity -> process_complexity(params, child_complexity) end) + end + + unquote_splicing(@chain_type_fields) + end + end + end +end + defmodule BlockScoutWeb.GraphQL.Schema.SmartContracts do @moduledoc false case Application.compile_env(:explorer, :chain_type) do @@ -42,7 +105,7 @@ end defmodule BlockScoutWeb.GraphQL.Schema.Types do @moduledoc false - require BlockScoutWeb.GraphQL.Schema.SmartContracts + require BlockScoutWeb.GraphQL.Schema.{Transaction, SmartContracts} use Absinthe.Schema.Notation use Absinthe.Relay.Schema.Notation, :modern @@ -50,11 +113,13 @@ defmodule BlockScoutWeb.GraphQL.Schema.Types do import Absinthe.Resolution.Helpers alias BlockScoutWeb.GraphQL.Resolvers.{ - InternalTransaction, + Token, + TokenTransfer, Transaction } alias BlockScoutWeb.GraphQL.Schema.SmartContracts, as: SmartContractsSchema + alias BlockScoutWeb.GraphQL.Schema.Transaction, as: TransactionSchema import_types(Absinthe.Type.Custom) import_types(BlockScoutWeb.GraphQL.Schema.Scalars) @@ -87,6 +152,13 @@ defmodule BlockScoutWeb.GraphQL.Schema.Types do complexity(fn params, child_complexity -> process_complexity(params, child_complexity) end) end + + connection field(:token_transfers, node_type: :token_transfer) do + arg(:count, :integer) + resolve(&TokenTransfer.get_by/3) + + complexity(fn params, child_complexity -> process_complexity(params, child_complexity) end) + end end @desc """ @@ -160,46 +232,37 @@ defmodule BlockScoutWeb.GraphQL.Schema.Types do field(:to_address_hash, :address_hash) field(:token_contract_address_hash, :address_hash) field(:transaction_hash, :full_hash) + + field :transaction, :transaction do + resolve(&Transaction.get_by/3) + end + + field :token, :token do + resolve(&Token.get_by/3) + end end @desc """ - Models a Web3 transaction. + Represents a token. """ - node object(:transaction, id_fetcher: &transaction_id_fetcher/2) do - field(:cumulative_gas_used, :decimal) - field(:error, :string) - field(:gas, :decimal) - field(:gas_price, :wei) - field(:gas_used, :decimal) - field(:hash, :full_hash) - field(:index, :integer) - field(:input, :string) - field(:nonce, :nonce_hash) - field(:r, :decimal) - field(:s, :decimal) - field(:status, :status) - field(:v, :decimal) - field(:value, :wei) - field(:block_hash, :full_hash) - field(:block_number, :integer) - field(:from_address_hash, :address_hash) - field(:to_address_hash, :address_hash) - field(:created_contract_address_hash, :address_hash) - field(:earliest_processing_start, :datetime) - field(:revert_reason, :string) - field(:max_priority_fee_per_gas, :wei) - field(:max_fee_per_gas, :wei) - field(:type, :integer) - field(:has_error_in_internal_txs, :boolean) - - connection field(:internal_transactions, node_type: :internal_transaction) do - arg(:count, :integer) - resolve(&InternalTransaction.get_by/3) - - complexity(fn params, child_complexity -> process_complexity(params, child_complexity) end) - end + object :token do + field(:name, :string) + field(:symbol, :string) + field(:total_supply, :decimal) + field(:decimals, :decimal) + field(:type, :string) + field(:holder_count, :integer) + field(:circulating_market_cap, :decimal) + field(:icon_url, :string) + field(:volume_24h, :decimal) + field(:contract_address_hash, :address_hash) end + @desc """ + Models a Web3 transaction. + """ + TransactionSchema.generate() + 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 diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 8c64622cb8..216d42c700 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -317,8 +317,8 @@ defmodule Explorer.Chain do filters = Keyword.get(options, :token_type) necessity_by_association = Keyword.get(options, :necessity_by_association) - direction - |> TokenTransfer.token_transfers_by_address_hash(address_hash, filters, paging_options) + address_hash + |> TokenTransfer.token_transfers_by_address_hash(direction, filters, paging_options) |> join_associations(necessity_by_association) |> select_repo(options).all() end diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index bafd52e39c..8bf41f8c88 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -442,7 +442,46 @@ defmodule Explorer.Chain.TokenTransfer do |> order_by([tt], desc: tt.block_number, desc: tt.log_index) end - def token_transfers_by_address_hash(direction, address_hash, token_types, paging_options) do + @doc """ + Retrieves token transfers associated with a given address, optionally filtered + by direction and token types. + + ## Parameters + + - `address_hash` (`Hash.Address.t()`): The address hash for which to retrieve + token transfers. + - `direction` (`nil | :to | :from`): The direction of the transfers to filter. + - `:to` - transfers where `to_address` matches `address_hash`. + - `:from` - transfers where `from_address` matches `address_hash`. + - `nil` - includes both incoming and outgoing transfers. + - `token_types` (`[binary()]`): The token types to filter, e.g `["ERC20", "ERC721"]`. + - `paging_options` (`nil | Explorer.PagingOptions.t()`): Pagination options to + limit the result set. + + ## Returns + + An `Ecto.Query` for `TokenTransfer.t()`. + + ## Examples + + Fetch all incoming ERC20 token transfers for a specific address: + + # iex> query = token_transfers_by_address_hash(address_hash, :to, ["ERC20"], paging_options) + # iex> Repo.all(query) + + Fetch both incoming and outgoing token transfers for a specific address + without pagination, token type filtering, and direction filtering: + + # iex> query = token_transfers_by_address_hash(address_hash, nil, [], nil) + # iex> Repo.all(query) + """ + @spec token_transfers_by_address_hash( + Hash.Address.t(), + nil | :to | :from, + [binary()], + nil | Explorer.PagingOptions.t() + ) :: Ecto.Query.t() + def token_transfers_by_address_hash(address_hash, direction, token_types, paging_options) do if direction == :to || direction == :from do only_consensus_transfers_query() |> filter_by_direction(direction, address_hash) @@ -474,7 +513,7 @@ defmodule Explorer.Chain.TokenTransfer do |> union(^from_address_hash_query) |> Chain.wrapped_union_subquery() |> order_by([tt], desc: tt.block_number, desc: tt.log_index) - |> limit(^paging_options.page_size) + |> handle_paging_options(paging_options) end end diff --git a/apps/explorer/lib/explorer/graphql.ex b/apps/explorer/lib/explorer/graphql.ex index feb318fde3..e275631615 100644 --- a/apps/explorer/lib/explorer/graphql.ex +++ b/apps/explorer/lib/explorer/graphql.ex @@ -14,6 +14,7 @@ defmodule Explorer.GraphQL do alias Explorer.Chain.{ Hash, InternalTransaction, + Token, TokenTransfer, Transaction } @@ -83,6 +84,18 @@ defmodule Explorer.GraphQL do end end + @doc """ + Returns a token for a given contract address hash. + """ + @spec get_token(map()) :: {:ok, Token.t()} | {:error, String.t()} + def get_token(%{contract_address_hash: _} = clauses) do + if token = Repo.replica().get_by(Token, clauses) do + {:ok, token} + else + {:error, "Token not found."} + end + end + @doc """ Returns a query to fetch token transfers for a token contract address hash. diff --git a/apps/explorer/lib/explorer/graphql/celo.ex b/apps/explorer/lib/explorer/graphql/celo.ex new file mode 100644 index 0000000000..3b92d12082 --- /dev/null +++ b/apps/explorer/lib/explorer/graphql/celo.ex @@ -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