Skip to content

Commit df52e38

Browse files
committed
feat: Add GraphQL API endpoints for Signet orders and fills
Phase 2 of ENG-1876: Signet Order Tracking and Indexing This adds GraphQL API support for querying Signet orders and fills: **New GraphQL Types (apps/block_scout_web/lib/block_scout_web/graphql/signet/):** - schema/types.ex: signet_order and signet_fill node objects - schema/query_fields.ex: Query fields with pagination support **New Resolvers:** - resolvers/order.ex: get_by and list resolvers for orders - resolvers/fill.ex: get_by and list resolvers for fills **New Explorer Module (apps/explorer/lib/explorer/graphql/signet.ex):** - get_order/2: Fetch single order by transaction_hash + log_index - orders_query/1: Paginated orders with block range filters - get_fill/3: Fetch single fill by chain_type + transaction_hash + log_index - fills_query/1: Paginated fills with chain_type and block range filters **GraphQL Queries:** - signet_order(transaction_hash, log_index): Get single order - signet_orders(first, after, block_number_gte, block_number_lte): Paginated orders - signet_fill(chain_type, transaction_hash, log_index): Get single fill - signet_fills(first, after, chain_type, block_number_gte, block_number_lte): Paginated fills Uses compile-time chain_type check to conditionally include Signet schema when CHAIN_TYPE=signet, following Blockscout's chain-specific extension patterns. Builds on PR #2 (Phase 1 data models and indexer). Closes ENG-1876
1 parent b231272 commit df52e38

6 files changed

Lines changed: 356 additions & 1 deletion

File tree

apps/block_scout_web/lib/block_scout_web/graphql/schema.ex

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule BlockScoutWeb.GraphQL.Schema do
33

44
use Absinthe.Schema
55
use Absinthe.Relay.Schema, :modern
6-
use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity]
6+
use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity], chain_type: [:explorer, :chain_type]
77

88
alias Absinthe.Middleware.Dataloader, as: AbsintheDataloaderMiddleware
99
alias Absinthe.Plugin, as: AbsinthePlugin
@@ -23,12 +23,21 @@ defmodule BlockScoutWeb.GraphQL.Schema do
2323
alias Explorer.Chain.TokenTransfer, as: ExplorerChainTokenTransfer
2424
alias Explorer.Chain.Transaction, as: ExplorerChainTransaction
2525

26+
if @chain_type == :signet do
27+
alias Explorer.Chain.Signet.Order, as: ExplorerChainSignetOrder
28+
alias Explorer.Chain.Signet.Fill, as: ExplorerChainSignetFill
29+
end
30+
2631
import_types(BlockScoutWeb.GraphQL.Schema.Types)
2732

2833
if @chain_identity == {:optimism, :celo} do
2934
import_types(BlockScoutWeb.GraphQL.Celo.Schema.Types)
3035
end
3136

37+
if @chain_type == :signet do
38+
import_types(BlockScoutWeb.GraphQL.Signet.Schema.Types)
39+
end
40+
3241
node interface do
3342
resolve_type(fn
3443
%ExplorerChainInternalTransaction{}, _ ->
@@ -114,6 +123,13 @@ defmodule BlockScoutWeb.GraphQL.Schema do
114123

115124
QueryFields.generate()
116125
end
126+
127+
if @chain_type == :signet do
128+
require BlockScoutWeb.GraphQL.Signet.QueryFields
129+
alias BlockScoutWeb.GraphQL.Signet.QueryFields, as: SignetQueryFields
130+
131+
SignetQueryFields.generate()
132+
end
117133
end
118134

119135
subscription do
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule BlockScoutWeb.GraphQL.Signet.Resolvers.Fill do
2+
@moduledoc """
3+
Resolvers for Signet fills, used in the Signet GraphQL schema.
4+
"""
5+
6+
alias Absinthe.Relay.Connection
7+
alias Explorer.Chain
8+
alias Explorer.GraphQL.Signet, as: GraphQL
9+
alias Explorer.Repo
10+
11+
@doc """
12+
Gets a single fill by chain type, transaction hash, and log index.
13+
"""
14+
def get_by(_parent, %{chain_type: chain_type_string, transaction_hash: hash_string, log_index: log_index}, _resolution) do
15+
with {:ok, hash} <- Chain.string_to_full_hash(hash_string),
16+
{:ok, chain_type} <- parse_chain_type(chain_type_string) do
17+
case GraphQL.get_fill(chain_type, hash, log_index) do
18+
nil -> {:error, "Fill not found"}
19+
fill -> {:ok, fill}
20+
end
21+
end
22+
end
23+
24+
@doc """
25+
Lists fills with optional filters and pagination.
26+
"""
27+
def list(_parent, args, _resolution) do
28+
args
29+
|> maybe_parse_chain_type_filter()
30+
|> GraphQL.fills_query()
31+
|> Connection.from_query(&Repo.all/1, args, options(args))
32+
end
33+
34+
defp parse_chain_type("rollup"), do: {:ok, :rollup}
35+
defp parse_chain_type("host"), do: {:ok, :host}
36+
defp parse_chain_type(_), do: {:error, "Invalid chain_type. Must be 'rollup' or 'host'"}
37+
38+
defp maybe_parse_chain_type_filter(%{chain_type: chain_type_string} = args) do
39+
case parse_chain_type(chain_type_string) do
40+
{:ok, chain_type} -> Map.put(args, :chain_type, chain_type)
41+
{:error, _} -> args
42+
end
43+
end
44+
45+
defp maybe_parse_chain_type_filter(args), do: args
46+
47+
defp options(%{before: _}), do: []
48+
defp options(%{count: count}), do: [count: count]
49+
defp options(_), do: []
50+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
defmodule BlockScoutWeb.GraphQL.Signet.Resolvers.Order do
2+
@moduledoc """
3+
Resolvers for Signet orders, used in the Signet GraphQL schema.
4+
"""
5+
6+
alias Absinthe.Relay.Connection
7+
alias Explorer.Chain
8+
alias Explorer.GraphQL.Signet, as: GraphQL
9+
alias Explorer.Repo
10+
11+
@doc """
12+
Gets a single order by transaction hash and log index.
13+
"""
14+
def get_by(_parent, %{transaction_hash: hash_string, log_index: log_index}, _resolution) do
15+
with {:ok, hash} <- Chain.string_to_full_hash(hash_string) do
16+
case GraphQL.get_order(hash, log_index) do
17+
nil -> {:error, "Order not found"}
18+
order -> {:ok, order}
19+
end
20+
end
21+
end
22+
23+
@doc """
24+
Lists orders with optional filters and pagination.
25+
"""
26+
def list(_parent, args, _resolution) do
27+
args
28+
|> GraphQL.orders_query()
29+
|> Connection.from_query(&Repo.all/1, args, options(args))
30+
end
31+
32+
defp options(%{before: _}), do: []
33+
defp options(%{count: count}), do: [count: count]
34+
defp options(_), do: []
35+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
defmodule BlockScoutWeb.GraphQL.Signet.QueryFields do
2+
@moduledoc """
3+
Query fields for the Signet schema.
4+
"""
5+
6+
alias BlockScoutWeb.GraphQL.Signet.Resolvers.{Fill, Order}
7+
8+
use Absinthe.Schema.Notation
9+
use Absinthe.Relay.Schema, :modern
10+
11+
defmacro generate do
12+
quote do
13+
@desc "Gets a Signet order by transaction hash and log index."
14+
field :signet_order, :signet_order do
15+
arg(:transaction_hash, non_null(:full_hash))
16+
arg(:log_index, non_null(:integer))
17+
18+
resolve(&Order.get_by/3)
19+
end
20+
21+
@desc "Gets Signet orders with pagination."
22+
connection field(:signet_orders, node_type: :signet_order) do
23+
arg(:count, :integer)
24+
arg(:block_number_gte, :integer)
25+
arg(:block_number_lte, :integer)
26+
27+
resolve(&Order.list/3)
28+
29+
complexity(fn
30+
%{first: first}, child_complexity -> first * child_complexity
31+
%{last: last}, child_complexity -> last * child_complexity
32+
%{}, _child_complexity -> 0
33+
end)
34+
end
35+
36+
@desc "Gets a Signet fill by chain type, transaction hash, and log index."
37+
field :signet_fill, :signet_fill do
38+
arg(:chain_type, non_null(:string))
39+
arg(:transaction_hash, non_null(:full_hash))
40+
arg(:log_index, non_null(:integer))
41+
42+
resolve(&Fill.get_by/3)
43+
end
44+
45+
@desc "Gets Signet fills with pagination."
46+
connection field(:signet_fills, node_type: :signet_fill) do
47+
arg(:count, :integer)
48+
arg(:chain_type, :string)
49+
arg(:block_number_gte, :integer)
50+
arg(:block_number_lte, :integer)
51+
52+
resolve(&Fill.list/3)
53+
54+
complexity(fn
55+
%{first: first}, child_complexity -> first * child_complexity
56+
%{last: last}, child_complexity -> last * child_complexity
57+
%{}, _child_complexity -> 0
58+
end)
59+
end
60+
end
61+
end
62+
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
defmodule BlockScoutWeb.GraphQL.Signet.Schema.Types do
2+
@moduledoc """
3+
GraphQL types for Signet orders and fills.
4+
"""
5+
6+
use Absinthe.Schema.Notation
7+
use Absinthe.Relay.Schema.Notation, :modern
8+
9+
@desc """
10+
Represents a Signet cross-chain order from the RollupOrders contract.
11+
12+
Orders specify inputs (tokens offered by the maker) and outputs (tokens
13+
expected in return). The chainId in outputs represents the DESTINATION
14+
chain where assets should be delivered.
15+
"""
16+
node object(:signet_order, id_fetcher: &signet_order_id_fetcher/2) do
17+
field(:transaction_hash, :full_hash)
18+
field(:log_index, :integer)
19+
field(:block_number, :integer)
20+
field(:deadline, :integer)
21+
field(:inputs_json, :string)
22+
field(:outputs_json, :string)
23+
field(:sweep_recipient, :address_hash)
24+
field(:sweep_token, :address_hash)
25+
field(:sweep_amount, :decimal)
26+
field(:inserted_at, :datetime)
27+
end
28+
29+
@desc """
30+
Represents a Signet fill event from RollupOrders or HostOrders contracts.
31+
32+
Fills record the execution of orders. The chainId in outputs represents
33+
the ORIGIN chain where the order was created, not where the fill occurred.
34+
"""
35+
node object(:signet_fill, id_fetcher: &signet_fill_id_fetcher/2) do
36+
field(:chain_type, :string)
37+
field(:transaction_hash, :full_hash)
38+
field(:log_index, :integer)
39+
field(:block_number, :integer)
40+
field(:outputs_json, :string)
41+
field(:inserted_at, :datetime)
42+
end
43+
44+
connection(node_type: :signet_order)
45+
connection(node_type: :signet_fill)
46+
47+
defp signet_order_id_fetcher(%{transaction_hash: transaction_hash, log_index: log_index}, _) do
48+
Jason.encode!(%{
49+
transaction_hash: to_string(transaction_hash),
50+
log_index: log_index
51+
})
52+
end
53+
54+
defp signet_fill_id_fetcher(
55+
%{chain_type: chain_type, transaction_hash: transaction_hash, log_index: log_index},
56+
_
57+
) do
58+
Jason.encode!(%{
59+
chain_type: to_string(chain_type),
60+
transaction_hash: to_string(transaction_hash),
61+
log_index: log_index
62+
})
63+
end
64+
end
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
defmodule Explorer.GraphQL.Signet do
2+
@moduledoc """
3+
Defines Ecto queries to fetch Signet order and fill data for the GraphQL schema.
4+
5+
Includes functions to construct queries for orders and fills, supporting
6+
pagination and filtering by block range and chain type.
7+
"""
8+
9+
import Ecto.Query, only: [from: 2, order_by: 3, where: 3]
10+
11+
alias Explorer.Chain.Hash
12+
alias Explorer.Chain.Signet.{Fill, Order}
13+
alias Explorer.Repo
14+
15+
@doc """
16+
Gets a single order by transaction hash and log index.
17+
18+
## Parameters
19+
- transaction_hash: the full transaction hash
20+
- log_index: the log index within the transaction
21+
22+
## Returns
23+
- Order struct or nil
24+
"""
25+
@spec get_order(Hash.Full.t(), integer()) :: Order.t() | nil
26+
def get_order(transaction_hash, log_index) do
27+
Repo.one(
28+
from(o in Order,
29+
where: o.transaction_hash == ^transaction_hash and o.log_index == ^log_index
30+
)
31+
)
32+
end
33+
34+
@doc """
35+
Constructs a query for orders with optional filters.
36+
37+
## Parameters
38+
- args: Map with optional filters:
39+
- block_number_gte: minimum block number
40+
- block_number_lte: maximum block number
41+
42+
## Returns
43+
- Ecto query
44+
"""
45+
@spec orders_query(map()) :: Ecto.Query.t()
46+
def orders_query(args \\ %{}) do
47+
from(o in Order, as: :order)
48+
|> maybe_filter_block_range(args, :order)
49+
|> order_by([order: o], desc: o.block_number, desc: o.log_index)
50+
end
51+
52+
@doc """
53+
Gets a single fill by chain type, transaction hash, and log index.
54+
55+
## Parameters
56+
- chain_type: :rollup or :host
57+
- transaction_hash: the full transaction hash
58+
- log_index: the log index within the transaction
59+
60+
## Returns
61+
- Fill struct or nil
62+
"""
63+
@spec get_fill(atom(), Hash.Full.t(), integer()) :: Fill.t() | nil
64+
def get_fill(chain_type, transaction_hash, log_index) do
65+
Repo.one(
66+
from(f in Fill,
67+
where:
68+
f.chain_type == ^chain_type and
69+
f.transaction_hash == ^transaction_hash and
70+
f.log_index == ^log_index
71+
)
72+
)
73+
end
74+
75+
@doc """
76+
Constructs a query for fills with optional filters.
77+
78+
## Parameters
79+
- args: Map with optional filters:
80+
- chain_type: :rollup or :host atom
81+
- block_number_gte: minimum block number
82+
- block_number_lte: maximum block number
83+
84+
## Returns
85+
- Ecto query
86+
"""
87+
@spec fills_query(map()) :: Ecto.Query.t()
88+
def fills_query(args \\ %{}) do
89+
from(f in Fill, as: :fill)
90+
|> maybe_filter_chain_type(args)
91+
|> maybe_filter_block_range(args, :fill)
92+
|> order_by([fill: f], desc: f.block_number, desc: f.log_index)
93+
end
94+
95+
# Private helper to filter by chain_type
96+
defp maybe_filter_chain_type(query, %{chain_type: chain_type}) when chain_type in [:rollup, :host] do
97+
where(query, [fill: f], f.chain_type == ^chain_type)
98+
end
99+
100+
defp maybe_filter_chain_type(query, _), do: query
101+
102+
# Private helper to filter by block range
103+
defp maybe_filter_block_range(query, args, binding) do
104+
query
105+
|> maybe_filter_block_gte(args, binding)
106+
|> maybe_filter_block_lte(args, binding)
107+
end
108+
109+
defp maybe_filter_block_gte(query, %{block_number_gte: block_number}, :order) do
110+
where(query, [order: o], o.block_number >= ^block_number)
111+
end
112+
113+
defp maybe_filter_block_gte(query, %{block_number_gte: block_number}, :fill) do
114+
where(query, [fill: f], f.block_number >= ^block_number)
115+
end
116+
117+
defp maybe_filter_block_gte(query, _, _), do: query
118+
119+
defp maybe_filter_block_lte(query, %{block_number_lte: block_number}, :order) do
120+
where(query, [order: o], o.block_number <= ^block_number)
121+
end
122+
123+
defp maybe_filter_block_lte(query, %{block_number_lte: block_number}, :fill) do
124+
where(query, [fill: f], f.block_number <= ^block_number)
125+
end
126+
127+
defp maybe_filter_block_lte(query, _, _), do: query
128+
end

0 commit comments

Comments
 (0)