From f446d01aad6f7f58d70051fac2b6a06c11f6d467 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 16 Feb 2026 04:08:16 +0000 Subject: [PATCH 01/24] feat: Add Signet Orders Indexer for ENG-1894 Implements indexer fetcher for Signet order tracking: ## New Files Database Schema: - apps/explorer/lib/explorer/chain/signet/order.ex - apps/explorer/lib/explorer/chain/signet/fill.ex Migration: - apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs Import Runners: - apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex - apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex Fetcher: - apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex - apps/indexer/lib/indexer/fetcher/signet/event_parser.ex - apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex - apps/indexer/lib/indexer/fetcher/signet/utils/db.ex - apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex ## Features - Parse Order, Filled, Sweep events from RollupOrders contract - Parse Filled events from HostOrders contract - Compute outputs_witness_hash for cross-chain correlation - Insert into signet_orders / signet_fills tables - Handle chain reorgs gracefully - Add metrics/logging for indexer health ## Modified Files - apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex - Added signet runners to chain type specific import stage - apps/indexer/lib/indexer/supervisor.ex - Added SignetOrdersFetcher to supervisor Closes ENG-1894 --- .../chain/import/runner/signet/fills.ex | 106 ++++ .../chain/import/runner/signet/orders.ex | 114 +++++ .../chain/import/stage/chain_type_specific.ex | 4 + .../lib/explorer/chain/signet/fill.ex | 70 +++ .../lib/explorer/chain/signet/order.ex | 82 +++ .../20260216040000_create_signet_tables.exs | 53 ++ .../lib/indexer/fetcher/signet/README.md | 118 +++++ .../indexer/fetcher/signet/event_parser.ex | 395 +++++++++++++++ .../indexer/fetcher/signet/orders_fetcher.ex | 476 ++++++++++++++++++ .../signet/orders_fetcher/supervisor.ex | 36 ++ .../indexer/fetcher/signet/reorg_handler.ex | 110 ++++ .../lib/indexer/fetcher/signet/utils/db.ex | 125 +++++ apps/indexer/lib/indexer/supervisor.ex | 4 + 13 files changed, 1693 insertions(+) create mode 100644 apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex create mode 100644 apps/explorer/lib/explorer/chain/signet/fill.ex create mode 100644 apps/explorer/lib/explorer/chain/signet/order.ex create mode 100644 apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs create mode 100644 apps/indexer/lib/indexer/fetcher/signet/README.md create mode 100644 apps/indexer/lib/indexer/fetcher/signet/event_parser.ex create mode 100644 apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex create mode 100644 apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex create mode 100644 apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex create mode 100644 apps/indexer/lib/indexer/fetcher/signet/utils/db.ex diff --git a/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex b/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex new file mode 100644 index 000000000000..2bf2aaa531fe --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex @@ -0,0 +1,106 @@ +defmodule Explorer.Chain.Import.Runner.Signet.Fills do + @moduledoc """ + Bulk imports of Explorer.Chain.Signet.Fill. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Signet.Fill + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Fill.t()] + + @impl Import.Runner + def ecto_schema_module, do: Fill + + @impl Import.Runner + def option_key, do: :signet_fills + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, Fill.insert_result_key(), fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :signet_fills, + :signet_fills + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [Fill.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce Fill ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.outputs_witness_hash, &1.chain_type}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:outputs_witness_hash, :chain_type], + on_conflict: on_conflict, + for: Fill, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + f in Fill, + update: [ + set: [ + # Don't update composite primary key fields + block_number: fragment("COALESCE(EXCLUDED.block_number, ?)", f.block_number), + transaction_hash: fragment("COALESCE(EXCLUDED.transaction_hash, ?)", f.transaction_hash), + log_index: fragment("COALESCE(EXCLUDED.log_index, ?)", f.log_index), + outputs_json: fragment("COALESCE(EXCLUDED.outputs_json, ?)", f.outputs_json), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", f.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", f.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.block_number, EXCLUDED.transaction_hash, EXCLUDED.log_index, EXCLUDED.outputs_json) IS DISTINCT FROM (?, ?, ?, ?)", + f.block_number, + f.transaction_hash, + f.log_index, + f.outputs_json + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex b/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex new file mode 100644 index 000000000000..de8c5eb7634b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex @@ -0,0 +1,114 @@ +defmodule Explorer.Chain.Import.Runner.Signet.Orders do + @moduledoc """ + Bulk imports of Explorer.Chain.Signet.Order. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Signet.Order + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Order.t()] + + @impl Import.Runner + def ecto_schema_module, do: Order + + @impl Import.Runner + def option_key, do: :signet_orders + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, Order.insert_result_key(), fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :signet_orders, + :signet_orders + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [Order.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce Order ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.outputs_witness_hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:outputs_witness_hash], + on_conflict: on_conflict, + for: Order, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + o in Order, + update: [ + set: [ + # Don't update outputs_witness_hash as it's the primary key + deadline: fragment("COALESCE(EXCLUDED.deadline, ?)", o.deadline), + block_number: fragment("COALESCE(EXCLUDED.block_number, ?)", o.block_number), + transaction_hash: fragment("COALESCE(EXCLUDED.transaction_hash, ?)", o.transaction_hash), + log_index: fragment("COALESCE(EXCLUDED.log_index, ?)", o.log_index), + inputs_json: fragment("COALESCE(EXCLUDED.inputs_json, ?)", o.inputs_json), + outputs_json: fragment("COALESCE(EXCLUDED.outputs_json, ?)", o.outputs_json), + sweep_recipient: fragment("COALESCE(EXCLUDED.sweep_recipient, ?)", o.sweep_recipient), + sweep_token: fragment("COALESCE(EXCLUDED.sweep_token, ?)", o.sweep_token), + sweep_amount: fragment("COALESCE(EXCLUDED.sweep_amount, ?)", o.sweep_amount), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", o.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", o.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.deadline, EXCLUDED.block_number, EXCLUDED.transaction_hash, EXCLUDED.log_index, EXCLUDED.sweep_recipient, EXCLUDED.sweep_token, EXCLUDED.sweep_amount) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + o.deadline, + o.block_number, + o.transaction_hash, + o.log_index, + o.sweep_recipient, + o.sweep_token, + o.sweep_amount + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index ae42f2172bf7..b174c178af79 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -68,6 +68,10 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do ], stability: [ Runner.Stability.Validators + ], + signet: [ + Runner.Signet.Orders, + Runner.Signet.Fills ] } diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex new file mode 100644 index 000000000000..7062617fe26a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -0,0 +1,70 @@ +defmodule Explorer.Chain.Signet.Fill do + @moduledoc """ + Models a Signet Filled event from RollupOrders or HostOrders contracts. + + Changes in the schema should be reflected in the bulk import module: + - Explorer.Chain.Import.Runner.Signet.Fills + + Migrations: + - Explorer.Repo.Signet.Migrations.CreateSignetTables + """ + + use Explorer.Schema + + alias Explorer.Chain.Hash + + @insert_result_key :insert_signet_fills + + @optional_attrs ~w()a + + @required_attrs ~w(outputs_witness_hash chain_type block_number transaction_hash log_index outputs_json)a + + @allowed_attrs @optional_attrs ++ @required_attrs + + @typedoc """ + Descriptor of a Signet Filled event: + * `outputs_witness_hash` - keccak256 hash of outputs for correlation with orders + * `chain_type` - Whether this fill occurred on :rollup or :host chain + * `block_number` - The block number where the fill was executed + * `transaction_hash` - The hash of the transaction containing the fill + * `log_index` - The index of the log within the transaction + * `outputs_json` - JSON-encoded array of filled outputs + """ + @type to_import :: %{ + outputs_witness_hash: binary(), + chain_type: :rollup | :host, + block_number: non_neg_integer(), + transaction_hash: binary(), + log_index: non_neg_integer(), + outputs_json: String.t() + } + + @primary_key false + typed_schema "signet_fills" do + field(:outputs_witness_hash, Hash.Full, primary_key: true) + field(:chain_type, Ecto.Enum, values: [:rollup, :host], primary_key: true) + field(:block_number, :integer) + field(:transaction_hash, Hash.Full) + field(:log_index, :integer) + field(:outputs_json, :string) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = fill, attrs \\ %{}) do + fill + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + |> unique_constraint([:outputs_witness_hash, :chain_type]) + end + + @doc """ + Shared result key used by import runners to return inserted Signet fills. + """ + @spec insert_result_key() :: atom() + def insert_result_key, do: @insert_result_key +end diff --git a/apps/explorer/lib/explorer/chain/signet/order.ex b/apps/explorer/lib/explorer/chain/signet/order.ex new file mode 100644 index 000000000000..22de10cf4d02 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/signet/order.ex @@ -0,0 +1,82 @@ +defmodule Explorer.Chain.Signet.Order do + @moduledoc """ + Models a Signet Order event from the RollupOrders contract. + + Changes in the schema should be reflected in the bulk import module: + - Explorer.Chain.Import.Runner.Signet.Orders + + Migrations: + - Explorer.Repo.Signet.Migrations.CreateSignetTables + """ + + use Explorer.Schema + + alias Explorer.Chain.{Hash, Wei} + + @insert_result_key :insert_signet_orders + + @optional_attrs ~w(sweep_recipient sweep_token sweep_amount)a + + @required_attrs ~w(outputs_witness_hash deadline block_number transaction_hash log_index inputs_json outputs_json)a + + @allowed_attrs @optional_attrs ++ @required_attrs + + @typedoc """ + Descriptor of a Signet Order event: + * `outputs_witness_hash` - keccak256 hash of outputs for cross-chain correlation with fills + * `deadline` - The deadline timestamp for the order + * `block_number` - The block number where the order was created + * `transaction_hash` - The hash of the transaction containing the order + * `log_index` - The index of the log within the transaction + * `inputs_json` - JSON-encoded array of input tokens and amounts + * `outputs_json` - JSON-encoded array of output tokens, amounts, and recipients + * `sweep_recipient` - Recipient address from Sweep event (if any) + * `sweep_token` - Token address from Sweep event (if any) + * `sweep_amount` - Amount from Sweep event (if any) + """ + @type to_import :: %{ + outputs_witness_hash: binary(), + deadline: non_neg_integer(), + block_number: non_neg_integer(), + transaction_hash: binary(), + log_index: non_neg_integer(), + inputs_json: String.t(), + outputs_json: String.t(), + sweep_recipient: binary() | nil, + sweep_token: binary() | nil, + sweep_amount: Decimal.t() | nil + } + + @primary_key false + typed_schema "signet_orders" do + field(:outputs_witness_hash, Hash.Full, primary_key: true) + field(:deadline, :integer) + field(:block_number, :integer) + field(:transaction_hash, Hash.Full) + field(:log_index, :integer) + field(:inputs_json, :string) + field(:outputs_json, :string) + field(:sweep_recipient, Hash.Address) + field(:sweep_token, Hash.Address) + field(:sweep_amount, Wei) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = order, attrs \\ %{}) do + order + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:outputs_witness_hash) + end + + @doc """ + Shared result key used by import runners to return inserted Signet orders. + """ + @spec insert_result_key() :: atom() + def insert_result_key, do: @insert_result_key +end diff --git a/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs b/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs new file mode 100644 index 000000000000..a4210c738c04 --- /dev/null +++ b/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs @@ -0,0 +1,53 @@ +defmodule Explorer.Repo.Signet.Migrations.CreateSignetTables do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE signet_fill_chain_type AS ENUM ('rollup', 'host')", + "DROP TYPE signet_fill_chain_type" + ) + + create table(:signet_orders, primary_key: false) do + # Primary key: outputs_witness_hash uniquely identifies an order + add(:outputs_witness_hash, :bytea, null: false, primary_key: true) + add(:deadline, :bigint, null: false) + add(:block_number, :bigint, null: false) + add(:transaction_hash, :bytea, null: false) + add(:log_index, :integer, null: false) + # JSON-encoded input/output arrays for flexibility + add(:inputs_json, :text, null: false) + add(:outputs_json, :text, null: false) + # Sweep event data (nullable - only present if Sweep was emitted) + add(:sweep_recipient, :bytea, null: true) + add(:sweep_token, :bytea, null: true) + add(:sweep_amount, :numeric, precision: 100, null: true) + timestamps(null: false, type: :utc_datetime_usec) + end + + # Index for querying orders by block for reorg handling + create(index(:signet_orders, [:block_number])) + # Index for querying orders by transaction + create(index(:signet_orders, [:transaction_hash])) + # Index for finding unfilled orders by deadline + create(index(:signet_orders, [:deadline])) + + create table(:signet_fills, primary_key: false) do + # Composite primary key: witness_hash + chain_type + # An order can be filled on both rollup and host chains + add(:outputs_witness_hash, :bytea, null: false, primary_key: true) + add(:chain_type, :signet_fill_chain_type, null: false, primary_key: true) + add(:block_number, :bigint, null: false) + add(:transaction_hash, :bytea, null: false) + add(:log_index, :integer, null: false) + add(:outputs_json, :text, null: false) + timestamps(null: false, type: :utc_datetime_usec) + end + + # Index for querying fills by block for reorg handling + create(index(:signet_fills, [:chain_type, :block_number])) + # Index for querying fills by transaction + create(index(:signet_fills, [:transaction_hash])) + # Index for correlating fills back to orders + create(index(:signet_fills, [:outputs_witness_hash])) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/README.md b/apps/indexer/lib/indexer/fetcher/signet/README.md new file mode 100644 index 000000000000..310f379fa4f3 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/README.md @@ -0,0 +1,118 @@ +# Signet Orders Fetcher + +This module indexes Order and Filled events from Signet's cross-chain order protocol. + +## Overview + +The Signet protocol enables cross-chain orders between a rollup (L2) and its host chain (L1). This fetcher: + +1. **Parses Order events** from the RollupOrders contract on L2 +2. **Parses Filled events** from both RollupOrders (L2) and HostOrders (L1) contracts +3. **Computes outputs_witness_hash** for correlating orders with their fills +4. **Handles chain reorgs** gracefully by removing invalidated data + +## Event Types + +### RollupOrders Contract (L2) + +- `Order(uint256 deadline, Input[] inputs, Output[] outputs)` - New order created +- `Filled(Output[] outputs)` - Order filled on rollup +- `Sweep(address recipient, address token, uint256 amount)` - Remaining funds swept + +### HostOrders Contract (L1) + +- `Filled(Output[] outputs)` - Order filled on host chain + +## Data Structures + +**Input:** `(address token, uint256 amount)` +**Output:** `(address recipient, address token, uint256 amount)` + +## Configuration + +Add to your config: + +```elixir +config :indexer, Indexer.Fetcher.Signet.OrdersFetcher, + enabled: true, + rollup_orders_address: "0x...", # RollupOrders contract on L2 + host_orders_address: "0x...", # HostOrders contract on L1 (optional) + l1_rpc: "https://...", # L1 RPC endpoint (optional, for host fills) + l1_rpc_block_range: 1000, # Max blocks to fetch per L1 request + recheck_interval: 15_000, # Milliseconds between checks + start_block: 0 # Starting block for indexing +``` + +## Database Tables + +### signet_orders + +Stores Order events with their inputs, outputs, and any associated Sweep data. + +| Column | Type | Description | +|--------|------|-------------| +| outputs_witness_hash | bytea | Primary key, keccak256 of outputs | +| deadline | bigint | Order deadline timestamp | +| block_number | bigint | Block where order was created | +| transaction_hash | bytea | Transaction containing the order | +| log_index | integer | Log index within transaction | +| inputs_json | text | JSON array of inputs | +| outputs_json | text | JSON array of outputs | +| sweep_recipient | bytea | Sweep recipient (if any) | +| sweep_token | bytea | Sweep token (if any) | +| sweep_amount | numeric | Sweep amount (if any) | + +### signet_fills + +Stores Filled events from both chains. + +| Column | Type | Description | +|--------|------|-------------| +| outputs_witness_hash | bytea | Part of composite primary key | +| chain_type | enum | 'rollup' or 'host' | +| block_number | bigint | Block where fill occurred | +| transaction_hash | bytea | Transaction containing the fill | +| log_index | integer | Log index within transaction | +| outputs_json | text | JSON array of filled outputs | + +## Cross-Chain Correlation + +Orders and fills are correlated using `outputs_witness_hash`: + +``` +outputs_witness_hash = keccak256(concat(keccak256(abi_encode(output)) for output in outputs)) +``` + +This allows matching fills to their original orders even across different chains. + +## Reorg Handling + +When a chain reorganization is detected: + +1. **Rollup reorg**: Deletes all orders and rollup fills from the reorg block onward +2. **Host reorg**: Deletes only host fills from the reorg block onward + +The fetcher will re-process the affected blocks after cleanup. + +## Metrics + +The fetcher logs: +- Number of orders/fills processed per batch +- Block ranges being indexed +- Any parsing or import errors + +## Files + +- `orders_fetcher.ex` - Main fetcher module +- `event_parser.ex` - ABI decoding and witness hash computation +- `reorg_handler.ex` - Chain reorganization handling +- `utils/db.ex` - Database query utilities +- `orders_fetcher/supervisor.ex` - Supervisor for the fetcher + +## Migration + +Run the migration to create tables: + +```bash +mix ecto.migrate --migrations-path apps/explorer/priv/signet/migrations +``` diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex new file mode 100644 index 000000000000..1e2b5bcac54d --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -0,0 +1,395 @@ +defmodule Indexer.Fetcher.Signet.EventParser do + @moduledoc """ + Parses Signet Order and Filled events from transaction logs. + + Handles ABI decoding for: + - Order(uint256 deadline, Input[] inputs, Output[] outputs) + - Filled(Output[] outputs) + - Sweep(address recipient, address token, uint256 amount) + + Where: + - Input = (address token, uint256 amount) + - Output = (address recipient, address token, uint256 amount) + """ + + require Logger + + # Event topic hashes + @order_event_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Order(uint256,(address,uint256)[],(address,address,uint256)[])"), + case: :lower + ) + + @filled_event_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Filled((address,address,uint256)[])"), + case: :lower + ) + + @sweep_event_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Sweep(address,address,uint256)"), + case: :lower + ) + + @doc """ + Parse logs from the RollupOrders contract. + + Returns {:ok, {orders, fills}} where orders and fills are lists of maps + ready for database import. + """ + @spec parse_rollup_logs([map()]) :: {:ok, {[map()], [map()]}} + def parse_rollup_logs(logs) when is_list(logs) do + {orders, fills, sweeps} = + Enum.reduce(logs, {[], [], []}, fn log, {orders_acc, fills_acc, sweeps_acc} -> + topic = get_topic(log, 0) + + cond do + topic == @order_event_topic -> + case parse_order_event(log) do + {:ok, order} -> {[order | orders_acc], fills_acc, sweeps_acc} + {:error, reason} -> + Logger.warning("Failed to parse Order event: #{inspect(reason)}") + {orders_acc, fills_acc, sweeps_acc} + end + + topic == @filled_event_topic -> + case parse_filled_event(log) do + {:ok, fill} -> {orders_acc, [fill | fills_acc], sweeps_acc} + {:error, reason} -> + Logger.warning("Failed to parse Filled event: #{inspect(reason)}") + {orders_acc, fills_acc, sweeps_acc} + end + + topic == @sweep_event_topic -> + case parse_sweep_event(log) do + {:ok, sweep} -> {orders_acc, fills_acc, [sweep | sweeps_acc]} + {:error, reason} -> + Logger.warning("Failed to parse Sweep event: #{inspect(reason)}") + {orders_acc, fills_acc, sweeps_acc} + end + + true -> + {orders_acc, fills_acc, sweeps_acc} + end + end) + + # Associate sweeps with their corresponding orders by transaction hash + orders_with_sweeps = associate_sweeps_with_orders(orders, sweeps) + + {:ok, {Enum.reverse(orders_with_sweeps), Enum.reverse(fills)}} + end + + @doc """ + Parse Filled events from the HostOrders contract. + + Returns {:ok, fills} where fills is a list of maps ready for database import. + """ + @spec parse_host_filled_logs([map()]) :: {:ok, [map()]} + def parse_host_filled_logs(logs) when is_list(logs) do + fills = + logs + |> Enum.filter(fn log -> get_topic(log, 0) == @filled_event_topic end) + |> Enum.map(&parse_filled_event/1) + |> Enum.filter(fn + {:ok, _} -> true + {:error, reason} -> + Logger.warning("Failed to parse host Filled event: #{inspect(reason)}") + false + end) + |> Enum.map(fn {:ok, fill} -> fill end) + + {:ok, fills} + end + + @doc """ + Compute the outputs_witness_hash for a list of outputs. + + The hash is computed as: keccak256(concat(keccak256(abi_encode(output)) for output in outputs)) + """ + @spec compute_outputs_witness_hash([{binary(), binary(), non_neg_integer()}]) :: binary() + def compute_outputs_witness_hash(outputs) do + output_hashes = + outputs + |> Enum.map(fn {recipient, token, amount} -> + # ABI-encode each output as (address, address, uint256) + encoded = + <<0::size(96)>> <> + normalize_address(recipient) <> + <<0::size(96)>> <> + normalize_address(token) <> + <> + + ExKeccak.hash_256(encoded) + end) + |> Enum.join() + + ExKeccak.hash_256(output_hashes) + end + + # Parse Order event + defp parse_order_event(log) do + data = get_log_data(log) + + with {:ok, decoded} <- decode_order_data(data) do + {deadline, inputs, outputs} = decoded + + outputs_witness_hash = compute_outputs_witness_hash(outputs) + + order = %{ + outputs_witness_hash: outputs_witness_hash, + deadline: deadline, + block_number: parse_block_number(log), + transaction_hash: get_transaction_hash(log), + log_index: parse_log_index(log), + inputs_json: Jason.encode!(format_inputs(inputs)), + outputs_json: Jason.encode!(format_outputs(outputs)) + } + + {:ok, order} + end + end + + # Parse Filled event + defp parse_filled_event(log) do + data = get_log_data(log) + + with {:ok, outputs} <- decode_filled_data(data) do + outputs_witness_hash = compute_outputs_witness_hash(outputs) + + fill = %{ + outputs_witness_hash: outputs_witness_hash, + block_number: parse_block_number(log), + transaction_hash: get_transaction_hash(log), + log_index: parse_log_index(log), + outputs_json: Jason.encode!(format_outputs(outputs)) + } + + {:ok, fill} + end + end + + # Parse Sweep event + defp parse_sweep_event(log) do + data = get_log_data(log) + + with {:ok, {recipient, token, amount}} <- decode_sweep_data(data) do + sweep = %{ + transaction_hash: get_transaction_hash(log), + recipient: recipient, + token: token, + amount: amount + } + + {:ok, sweep} + end + end + + # Decode Order event data + # Order(uint256 deadline, Input[] inputs, Output[] outputs) + # Input = (address token, uint256 amount) + # Output = (address recipient, address token, uint256 amount) + defp decode_order_data(data) when is_binary(data) do + try do + # ABI decode: uint256, (address,uint256)[], (address,address,uint256)[] + # For dynamic arrays, we have offsets first, then the actual data + + <> = data + + # Parse inputs array + inputs_data = binary_part(rest, inputs_offset - 96, byte_size(rest) - inputs_offset + 96) + inputs = decode_input_array(inputs_data) + + # Parse outputs array + outputs_data = binary_part(rest, outputs_offset - 96, byte_size(rest) - outputs_offset + 96) + outputs = decode_output_array(outputs_data) + + {:ok, {deadline, inputs, outputs}} + rescue + e -> + Logger.error("Error decoding Order data: #{inspect(e)}") + {:error, :decode_failed} + end + end + + defp decode_order_data(_), do: {:error, :invalid_data} + + # Decode Filled event data + # Filled(Output[] outputs) + defp decode_filled_data(data) when is_binary(data) do + try do + <<_offset::unsigned-big-integer-size(256), rest::binary>> = data + outputs = decode_output_array(rest) + {:ok, outputs} + rescue + e -> + Logger.error("Error decoding Filled data: #{inspect(e)}") + {:error, :decode_failed} + end + end + + defp decode_filled_data(_), do: {:error, :invalid_data} + + # Decode Sweep event data + # Sweep(address recipient, address token, uint256 amount) + defp decode_sweep_data(data) when is_binary(data) do + try do + <<_padding1::binary-size(12), + recipient::binary-size(20), + _padding2::binary-size(12), + token::binary-size(20), + amount::unsigned-big-integer-size(256)>> = data + + {:ok, {recipient, token, amount}} + rescue + e -> + Logger.error("Error decoding Sweep data: #{inspect(e)}") + {:error, :decode_failed} + end + end + + defp decode_sweep_data(_), do: {:error, :invalid_data} + + # Decode array of Input tuples + defp decode_input_array(<>) do + decode_inputs(rest, length, []) + end + + defp decode_inputs(_data, 0, acc), do: Enum.reverse(acc) + + defp decode_inputs(<<_padding::binary-size(12), + token::binary-size(20), + amount::unsigned-big-integer-size(256), + rest::binary>>, count, acc) do + input = {token, amount} + decode_inputs(rest, count - 1, [input | acc]) + end + + # Decode array of Output tuples + defp decode_output_array(<>) do + decode_outputs(rest, length, []) + end + + defp decode_outputs(_data, 0, acc), do: Enum.reverse(acc) + + defp decode_outputs(<<_padding1::binary-size(12), + recipient::binary-size(20), + _padding2::binary-size(12), + token::binary-size(20), + amount::unsigned-big-integer-size(256), + rest::binary>>, count, acc) do + output = {recipient, token, amount} + decode_outputs(rest, count - 1, [output | acc]) + end + + # Associate sweep events with their corresponding orders by transaction hash + defp associate_sweeps_with_orders(orders, sweeps) do + sweeps_by_tx = Enum.group_by(sweeps, & &1.transaction_hash) + + Enum.map(orders, fn order -> + case Map.get(sweeps_by_tx, order.transaction_hash) do + [sweep | _] -> + Map.merge(order, %{ + sweep_recipient: sweep.recipient, + sweep_token: sweep.token, + sweep_amount: sweep.amount + }) + + _ -> + order + end + end) + end + + # Format inputs for JSON storage + defp format_inputs(inputs) do + Enum.map(inputs, fn {token, amount} -> + %{ + "token" => format_address(token), + "amount" => Integer.to_string(amount) + } + end) + end + + # Format outputs for JSON storage + defp format_outputs(outputs) do + Enum.map(outputs, fn {recipient, token, amount} -> + %{ + "recipient" => format_address(recipient), + "token" => format_address(token), + "amount" => Integer.to_string(amount) + } + end) + end + + defp format_address(bytes) when is_binary(bytes) and byte_size(bytes) == 20 do + "0x" <> Base.encode16(bytes, case: :lower) + end + + defp normalize_address(bytes) when is_binary(bytes) and byte_size(bytes) == 20, do: bytes + + defp normalize_address("0x" <> hex) when byte_size(hex) == 40 do + Base.decode16!(hex, case: :mixed) + end + + defp get_topic(log, index) do + topics = Map.get(log, "topics") || Map.get(log, :topics) || [] + Enum.at(topics, index) + end + + defp get_log_data(log) do + data = Map.get(log, "data") || Map.get(log, :data) || "" + + case data do + "0x" <> hex -> Base.decode16!(hex, case: :mixed) + hex when is_binary(hex) -> Base.decode16!(hex, case: :mixed) + _ -> "" + end + end + + defp get_transaction_hash(log) do + hash = Map.get(log, "transactionHash") || Map.get(log, :transaction_hash) + + case hash do + "0x" <> _ -> hash + bytes when is_binary(bytes) -> "0x" <> Base.encode16(bytes, case: :lower) + _ -> nil + end + end + + defp parse_block_number(log) do + block = Map.get(log, "blockNumber") || Map.get(log, :block_number) + + case block do + "0x" <> hex -> + {num, ""} = Integer.parse(hex, 16) + num + + num when is_integer(num) -> + num + + _ -> + 0 + end + end + + defp parse_log_index(log) do + index = Map.get(log, "logIndex") || Map.get(log, :log_index) + + case index do + "0x" <> hex -> + {num, ""} = Integer.parse(hex, 16) + num + + num when is_integer(num) -> + num + + _ -> + 0 + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex new file mode 100644 index 000000000000..63568d99d03f --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -0,0 +1,476 @@ +defmodule Indexer.Fetcher.Signet.OrdersFetcher do + @moduledoc """ + Fetcher for Signet Order and Filled events from RollupOrders and HostOrders contracts. + + This module tracks cross-chain orders in the Signet protocol by: + 1. Parsing Order events from the RollupOrders contract on L2 + 2. Parsing Filled events from both RollupOrders (L2) and HostOrders (L1) contracts + 3. Parsing Sweep events from RollupOrders contract + 4. Computing outputs_witness_hash for cross-chain correlation + 5. Inserting events into signet_orders / signet_fills tables + + ## Event Signatures + + RollupOrders contract: + - Order(uint256 deadline, Input[] inputs, Output[] outputs) + - Filled(Output[] outputs) + - Sweep(address recipient, address token, uint256 amount) + + HostOrders contract: + - Filled(Output[] outputs) + + ## Configuration + + The fetcher requires the following configuration in config.exs: + + config :indexer, Indexer.Fetcher.Signet.OrdersFetcher, + enabled: true, + rollup_orders_address: "0x...", + host_orders_address: "0x...", + l1_rpc: "https://...", + l1_rpc_block_range: 1000, + recheck_interval: 15_000 + + ## Architecture + + Uses a BufferedTask-based approach similar to Arbitrum fetchers, with tasks: + - `:check_new_rollup` - Discovers new events on rollup chain (L2) + - `:check_new_host` - Discovers new Filled events on host chain (L1) + - `:check_historical` - Backfills historical events + """ + + use Indexer.Fetcher, restart: :permanent + + require Logger + + alias Explorer.Chain + alias Explorer.Chain.Signet.{Order, Fill} + alias Indexer.BufferedTask + alias Indexer.Fetcher.Signet.{EventParser, ReorgHandler} + alias Indexer.Helper, as: IndexerHelper + + @behaviour BufferedTask + + # Event topic hashes (keccak256 of event signatures) + # Order(uint256,tuple[],tuple[]) + @order_event_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Order(uint256,(address,uint256)[],(address,address,uint256)[])"), + case: :lower + ) + + # Filled(tuple[]) + @filled_event_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Filled((address,address,uint256)[])"), + case: :lower + ) + + # Sweep(address,address,uint256) + @sweep_event_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Sweep(address,address,uint256)"), + case: :lower + ) + + # 250ms interval between processing buffered entries + @idle_interval 250 + @max_concurrency 1 + @max_batch_size 1 + + # 10 minutes cooldown for failed tasks + @cooldown_interval :timer.minutes(10) + + # Catchup interval for historical discovery + @catchup_recheck_interval :timer.seconds(2) + + @typep fetcher_task :: :check_new_rollup | :check_new_host | :check_historical + @typep queued_task :: :init_worker | {non_neg_integer(), fetcher_task()} + + def child_spec([init_options, gen_server_options]) do + {json_rpc_named_arguments, init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) + + config = Application.get_all_env(:indexer)[__MODULE__] || [] + + rollup_orders_address = config[:rollup_orders_address] + host_orders_address = config[:host_orders_address] + l1_rpc = config[:l1_rpc] + l1_rpc_block_range = config[:l1_rpc_block_range] || 1000 + recheck_interval = config[:recheck_interval] || 15_000 + start_block = config[:start_block] || 0 + + failure_interval_threshold = config[:failure_interval_threshold] || min(20 * recheck_interval, :timer.minutes(10)) + + intervals = %{ + check_new_rollup: recheck_interval, + check_new_host: recheck_interval, + check_historical: @catchup_recheck_interval + } + + initial_config = %{ + json_l2_rpc_named_arguments: json_rpc_named_arguments, + json_l1_rpc_named_arguments: if(l1_rpc, do: IndexerHelper.json_rpc_named_arguments(l1_rpc), else: nil), + rollup_orders_address: rollup_orders_address, + host_orders_address: host_orders_address, + l1_rpc_block_range: l1_rpc_block_range, + recheck_interval: recheck_interval, + failure_interval_threshold: failure_interval_threshold, + start_block: start_block + } + + initial_state = %{ + config: initial_config, + intervals: intervals, + task_data: %{}, + completed_tasks: %{} + } + + buffered_task_init_options = + defaults() + |> Keyword.merge(init_options) + |> Keyword.put(:state, initial_state) + + Supervisor.child_spec( + {BufferedTask, [{__MODULE__, buffered_task_init_options}, gen_server_options]}, + id: __MODULE__, + restart: :transient + ) + end + + defp defaults do + [ + flush_interval: @idle_interval, + max_concurrency: @max_concurrency, + max_batch_size: @max_batch_size, + poll: false, + task_supervisor: __MODULE__.TaskSupervisor, + metadata: [fetcher: :signet_orders_fetcher] + ] + end + + @impl BufferedTask + def init(initial, reducer, _state) do + reducer.(:init_worker, initial) + end + + @impl BufferedTask + @spec run([queued_task()], map()) :: {:ok, map()} | {:retry, [queued_task()], map()} | :retry + def run(tasks, state) + + def run([:init_worker], state) do + configured_state = initialize_workers(state) + + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + tasks_to_run = + [{now, :check_new_rollup}] + |> maybe_add_host_task(now, configured_state) + |> maybe_add_historical_task(now, configured_state) + + completion_state = %{ + check_historical: is_nil(configured_state.config.start_block) + } + + BufferedTask.buffer(__MODULE__, tasks_to_run, false) + + updated_state = Map.put(configured_state, :completed_tasks, completion_state) + {:ok, updated_state} + end + + def run([{timeout, task_tag}], state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + with {:timeout_elapsed, true} <- {:timeout_elapsed, timeout <= now}, + {:threshold_ok, true} <- {:threshold_ok, now - timeout <= state.config.failure_interval_threshold}, + {:runner_defined, runner} when not is_nil(runner) <- {:runner_defined, Map.get(task_runners(), task_tag)} do + runner.(state) + else + {:timeout_elapsed, false} -> + {:retry, [{timeout, task_tag}], state} + + {:threshold_ok, false} -> + new_timeout = now + @cooldown_interval + Logger.warning("Task #{task_tag} has been failing abnormally, applying cooldown") + {:retry, [{new_timeout, task_tag}], state} + + {:runner_defined, nil} -> + Logger.warning("Unknown task type: #{inspect(task_tag)}") + {:ok, state} + end + end + + defp task_runners do + %{ + check_new_rollup: &handle_check_new_rollup/1, + check_new_host: &handle_check_new_host/1, + check_historical: &handle_check_historical/1 + } + end + + defp initialize_workers(state) do + rollup_start_block = get_last_processed_block(:rollup, state.config.start_block) + host_start_block = get_last_processed_block(:host, state.config.start_block) + + task_data = %{ + check_new_rollup: %{ + start_block: rollup_start_block + }, + check_new_host: %{ + start_block: host_start_block + }, + check_historical: %{ + end_block: state.config.start_block + } + } + + %{state | task_data: task_data} + end + + defp maybe_add_host_task(tasks, now, state) do + if state.config.json_l1_rpc_named_arguments && state.config.host_orders_address do + [{now, :check_new_host} | tasks] + else + tasks + end + end + + defp maybe_add_historical_task(tasks, now, state) do + if state.config.start_block && state.config.start_block > 0 do + [{now, :check_historical} | tasks] + else + tasks + end + end + + defp handle_check_new_rollup(state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + case fetch_and_process_rollup_events(state) do + {:ok, updated_state} -> + next_run_time = now + updated_state.intervals[:check_new_rollup] + BufferedTask.buffer(__MODULE__, [{next_run_time, :check_new_rollup}], false) + {:ok, updated_state} + + {:error, reason} -> + Logger.error("Failed to fetch rollup events: #{inspect(reason)}") + {:retry, [{now + @cooldown_interval, :check_new_rollup}], state} + end + end + + defp handle_check_new_host(state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + case fetch_and_process_host_events(state) do + {:ok, updated_state} -> + next_run_time = now + updated_state.intervals[:check_new_host] + BufferedTask.buffer(__MODULE__, [{next_run_time, :check_new_host}], false) + {:ok, updated_state} + + {:error, reason} -> + Logger.error("Failed to fetch host events: #{inspect(reason)}") + {:retry, [{now + @cooldown_interval, :check_new_host}], state} + end + end + + defp handle_check_historical(state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + case fetch_historical_events(state) do + {:ok, updated_state, :continue} -> + next_run_time = now + updated_state.intervals[:check_historical] + BufferedTask.buffer(__MODULE__, [{next_run_time, :check_historical}], false) + {:ok, updated_state} + + {:ok, updated_state, :done} -> + Logger.info("Historical event discovery completed") + updated_state = put_in(updated_state.completed_tasks[:check_historical], true) + {:ok, updated_state} + + {:error, reason} -> + Logger.error("Failed to fetch historical events: #{inspect(reason)}") + {:retry, [{now + @cooldown_interval, :check_historical}], state} + end + end + + defp fetch_and_process_rollup_events(state) do + config = state.config + start_block = state.task_data.check_new_rollup.start_block + + with {:ok, latest_block} <- get_latest_block(config.json_l2_rpc_named_arguments), + {:ok, logs} <- + fetch_logs( + config.json_l2_rpc_named_arguments, + config.rollup_orders_address, + start_block, + latest_block + ), + {:ok, {orders, fills}} <- EventParser.parse_rollup_logs(logs), + :ok <- import_orders(orders), + :ok <- import_fills(fills, :rollup) do + Logger.info( + "Processed rollup events: #{length(orders)} orders, #{length(fills)} fills (blocks #{start_block}-#{latest_block})" + ) + + updated_task_data = put_in(state.task_data.check_new_rollup.start_block, latest_block + 1) + {:ok, %{state | task_data: updated_task_data}} + end + end + + defp fetch_and_process_host_events(state) do + config = state.config + start_block = state.task_data.check_new_host.start_block + + with {:ok, latest_block} <- get_latest_block(config.json_l1_rpc_named_arguments), + end_block = min(start_block + config.l1_rpc_block_range, latest_block), + {:ok, logs} <- + fetch_logs( + config.json_l1_rpc_named_arguments, + config.host_orders_address, + start_block, + end_block, + [@filled_event_topic] + ), + {:ok, fills} <- EventParser.parse_host_filled_logs(logs), + :ok <- import_fills(fills, :host) do + Logger.info( + "Processed host events: #{length(fills)} fills (blocks #{start_block}-#{end_block})" + ) + + updated_task_data = put_in(state.task_data.check_new_host.start_block, end_block + 1) + {:ok, %{state | task_data: updated_task_data}} + end + end + + defp fetch_historical_events(state) do + config = state.config + end_block = state.task_data.check_historical.end_block + + if end_block <= 0 do + {:ok, state, :done} + else + start_block = max(0, end_block - 1000) + + with {:ok, logs} <- + fetch_logs( + config.json_l2_rpc_named_arguments, + config.rollup_orders_address, + start_block, + end_block + ), + {:ok, {orders, fills}} <- EventParser.parse_rollup_logs(logs), + :ok <- import_orders(orders), + :ok <- import_fills(fills, :rollup) do + Logger.info("Processed historical events: #{length(orders)} orders (blocks #{start_block}-#{end_block})") + + updated_task_data = put_in(state.task_data.check_historical.end_block, start_block - 1) + status = if start_block <= 0, do: :done, else: :continue + {:ok, %{state | task_data: updated_task_data}, status} + end + end + end + + defp fetch_logs(json_rpc_named_arguments, contract_address, from_block, to_block, topics \\ nil) do + topics = topics || [@order_event_topic, @filled_event_topic, @sweep_event_topic] + + request = %{ + id: 1, + jsonrpc: "2.0", + method: "eth_getLogs", + params: [ + %{ + address: contract_address, + fromBlock: "0x#{Integer.to_string(from_block, 16)}", + toBlock: "0x#{Integer.to_string(to_block, 16)}", + topics: [topics] + } + ] + } + + case EthereumJSONRPC.json_rpc(request, json_rpc_named_arguments) do + {:ok, logs} -> {:ok, logs} + {:error, reason} -> {:error, reason} + end + end + + defp get_latest_block(json_rpc_named_arguments) do + request = %{ + id: 1, + jsonrpc: "2.0", + method: "eth_blockNumber", + params: [] + } + + case EthereumJSONRPC.json_rpc(request, json_rpc_named_arguments) do + {:ok, hex_block} -> + {block, ""} = Integer.parse(String.trim_leading(hex_block, "0x"), 16) + {:ok, block} + + {:error, reason} -> + {:error, reason} + end + end + + defp get_last_processed_block(chain_type, default_start) do + # Query database for last processed block + # Falls back to default_start if no records exist + case chain_type do + :rollup -> + case Explorer.Repo.one( + from(o in Order, + select: max(o.block_number) + ) + ) do + nil -> default_start + block -> block + 1 + end + + :host -> + case Explorer.Repo.one( + from(f in Fill, + where: f.chain_type == :host, + select: max(f.block_number) + ) + ) do + nil -> default_start + block -> block + 1 + end + end + end + + defp import_orders([]), do: :ok + + defp import_orders(orders) do + {:ok, _} = + Chain.import(%{ + signet_orders: %{params: orders}, + timeout: :infinity + }) + + :ok + end + + defp import_fills([], _chain_type), do: :ok + + defp import_fills(fills, chain_type) do + fills_with_chain = Enum.map(fills, &Map.put(&1, :chain_type, chain_type)) + + {:ok, _} = + Chain.import(%{ + signet_fills: %{params: fills_with_chain}, + timeout: :infinity + }) + + :ok + end + + @doc """ + Handle chain reorganization by removing events from invalidated blocks. + + Called when a reorg is detected to clean up data from blocks that are + no longer in the canonical chain. + """ + @spec handle_reorg(non_neg_integer(), :rollup | :host) :: :ok + def handle_reorg(from_block, chain_type) do + ReorgHandler.handle_reorg(from_block, chain_type) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex new file mode 100644 index 000000000000..93b314462148 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex @@ -0,0 +1,36 @@ +defmodule Indexer.Fetcher.Signet.OrdersFetcher.Supervisor do + @moduledoc """ + Supervises the Signet OrdersFetcher and its task supervisor. + """ + + use Supervisor + + alias Indexer.Fetcher.Signet.OrdersFetcher + + def child_spec(init_arg) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [init_arg]}, + type: :supervisor + } + end + + def start_link(init_arg) do + Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + def disabled? do + config = Application.get_env(:indexer, OrdersFetcher, []) + not Keyword.get(config, :enabled, false) + end + + @impl Supervisor + def init(init_arg) do + children = [ + {Task.Supervisor, name: OrdersFetcher.TaskSupervisor}, + {OrdersFetcher, init_arg} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex new file mode 100644 index 000000000000..1090786c7a71 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex @@ -0,0 +1,110 @@ +defmodule Indexer.Fetcher.Signet.ReorgHandler do + @moduledoc """ + Handles chain reorganizations for Signet order and fill data. + + When a chain reorg is detected, this module removes all orders and fills + from blocks that are no longer in the canonical chain, allowing them to + be re-indexed from the new canonical blocks. + """ + + require Logger + + import Ecto.Query + + alias Explorer.Chain.Signet.{Order, Fill} + alias Explorer.Repo + + @doc """ + Handle a chain reorganization starting from the given block number. + + Deletes all orders and fills at or after the reorg block, allowing + the fetcher to re-process these blocks. + + ## Parameters + - from_block: The block number where the reorg was detected + - chain_type: :rollup for L2 reorgs (affects orders and rollup fills), + :host for L1 reorgs (affects host fills only) + + ## Returns + - :ok + """ + @spec handle_reorg(non_neg_integer(), :rollup | :host) :: :ok + def handle_reorg(from_block, chain_type) do + Logger.info("Handling #{chain_type} chain reorg from block #{from_block}") + + case chain_type do + :rollup -> + handle_rollup_reorg(from_block) + + :host -> + handle_host_reorg(from_block) + end + + :ok + end + + # Handle reorg on the rollup (L2) chain + # This affects both orders and rollup fills + defp handle_rollup_reorg(from_block) do + # Delete orders at or after the reorg block + {deleted_orders, _} = + Repo.delete_all( + from(o in Order, + where: o.block_number >= ^from_block + ) + ) + + # Delete rollup fills at or after the reorg block + {deleted_fills, _} = + Repo.delete_all( + from(f in Fill, + where: f.chain_type == :rollup and f.block_number >= ^from_block + ) + ) + + Logger.info( + "Rollup reorg cleanup: deleted #{deleted_orders} orders, #{deleted_fills} fills from block #{from_block}" + ) + end + + # Handle reorg on the host (L1) chain + # This only affects host fills + defp handle_host_reorg(from_block) do + {deleted_fills, _} = + Repo.delete_all( + from(f in Fill, + where: f.chain_type == :host and f.block_number >= ^from_block + ) + ) + + Logger.info("Host reorg cleanup: deleted #{deleted_fills} fills from block #{from_block}") + end + + @doc """ + Check if a block is still valid in the chain by comparing its hash. + + Returns true if the block is still valid, false if it has been reorganized. + """ + @spec block_still_valid?(non_neg_integer(), binary(), keyword()) :: boolean() + def block_still_valid?(block_number, expected_hash, json_rpc_named_arguments) do + request = %{ + id: 1, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["0x#{Integer.to_string(block_number, 16)}", false] + } + + case EthereumJSONRPC.json_rpc(request, json_rpc_named_arguments) do + {:ok, block} when not is_nil(block) -> + actual_hash = Map.get(block, "hash") + normalize_hash(actual_hash) == normalize_hash(expected_hash) + + _ -> + false + end + end + + defp normalize_hash("0x" <> hex), do: String.downcase(hex) + defp normalize_hash(hex) when is_binary(hex), do: String.downcase(hex) + defp normalize_hash(_), do: nil +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex new file mode 100644 index 000000000000..faaa0ff86c86 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex @@ -0,0 +1,125 @@ +defmodule Indexer.Fetcher.Signet.Utils.Db do + @moduledoc """ + Database utility functions for Signet order indexing. + """ + + import Ecto.Query + + alias Explorer.Chain.Signet.{Order, Fill} + alias Explorer.Repo + + @doc """ + Get the highest indexed block number for orders. + Returns the default if no orders exist. + """ + @spec highest_indexed_order_block(non_neg_integer()) :: non_neg_integer() + def highest_indexed_order_block(default \\ 0) do + case Repo.one(from(o in Order, select: max(o.block_number))) do + nil -> default + block -> block + end + end + + @doc """ + Get the highest indexed block number for fills on a specific chain. + Returns the default if no fills exist. + """ + @spec highest_indexed_fill_block(:rollup | :host, non_neg_integer()) :: non_neg_integer() + def highest_indexed_fill_block(chain_type, default \\ 0) do + case Repo.one( + from(f in Fill, + where: f.chain_type == ^chain_type, + select: max(f.block_number) + ) + ) do + nil -> default + block -> block + end + end + + @doc """ + Get an order by its outputs_witness_hash. + """ + @spec get_order_by_witness_hash(binary()) :: Order.t() | nil + def get_order_by_witness_hash(witness_hash) do + Repo.one( + from(o in Order, + where: o.outputs_witness_hash == ^witness_hash + ) + ) + end + + @doc """ + Get all fills for a specific order by witness hash. + """ + @spec get_fills_for_order(binary()) :: [Fill.t()] + def get_fills_for_order(witness_hash) do + Repo.all( + from(f in Fill, + where: f.outputs_witness_hash == ^witness_hash, + order_by: [asc: f.chain_type, asc: f.block_number] + ) + ) + end + + @doc """ + Check if an order has been filled on a specific chain. + """ + @spec order_filled_on_chain?(binary(), :rollup | :host) :: boolean() + def order_filled_on_chain?(witness_hash, chain_type) do + Repo.exists?( + from(f in Fill, + where: f.outputs_witness_hash == ^witness_hash and f.chain_type == ^chain_type + ) + ) + end + + @doc """ + Get unfilled orders (orders without any corresponding fills). + """ + @spec get_unfilled_orders(non_neg_integer()) :: [Order.t()] + def get_unfilled_orders(limit \\ 100) do + Repo.all( + from(o in Order, + left_join: f in Fill, + on: o.outputs_witness_hash == f.outputs_witness_hash, + where: is_nil(f.outputs_witness_hash), + limit: ^limit, + order_by: [desc: o.block_number] + ) + ) + end + + @doc """ + Get orders by deadline range for monitoring. + """ + @spec get_orders_by_deadline_range(non_neg_integer(), non_neg_integer()) :: [Order.t()] + def get_orders_by_deadline_range(from_deadline, to_deadline) do + Repo.all( + from(o in Order, + where: o.deadline >= ^from_deadline and o.deadline <= ^to_deadline, + order_by: [asc: o.deadline] + ) + ) + end + + @doc """ + Count orders and fills for metrics. + """ + @spec get_order_fill_counts() :: %{orders: non_neg_integer(), rollup_fills: non_neg_integer(), host_fills: non_neg_integer()} + def get_order_fill_counts do + orders_count = Repo.one(from(o in Order, select: count(o.outputs_witness_hash))) + + rollup_fills_count = + Repo.one(from(f in Fill, where: f.chain_type == :rollup, select: count(f.outputs_witness_hash))) + + host_fills_count = + Repo.one(from(f in Fill, where: f.chain_type == :host, select: count(f.outputs_witness_hash))) + + %{ + orders: orders_count || 0, + rollup_fills: rollup_fills_count || 0, + host_fills: host_fills_count || 0 + } + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 057de8cc7305..365064e7311f 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -63,6 +63,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.Arbitrum.RollupMessagesCatchup, as: ArbitrumRollupMessagesCatchup alias Indexer.Fetcher.Arbitrum.TrackingBatchesStatuses, as: ArbitrumTrackingBatchesStatuses alias Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1, as: ArbitrumTrackingMessagesOnL1 + alias Indexer.Fetcher.Signet.OrdersFetcher, as: SignetOrdersFetcher alias Indexer.Fetcher.ZkSync.BatchesStatusTracker, as: ZkSyncBatchesStatusTracker alias Indexer.Fetcher.ZkSync.TransactionBatch, as: ZkSyncTransactionBatch @@ -250,6 +251,9 @@ defmodule Indexer.Supervisor do {ArbitrumMessagesToL2Matcher.Supervisor, [[memory_monitor: memory_monitor]]}, {ArbitrumDataBackfill.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, + configure(SignetOrdersFetcher.Supervisor, [ + [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] + ]), configure(Indexer.Fetcher.Celo.ValidatorGroupVotes.Supervisor, [ [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] ]), From e192282f9cc22a4503992df355923fcaef75f070 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 13:51:58 +0000 Subject: [PATCH 02/24] feat: integrate @signet-sh/sdk ABIs for Signet Orders indexer - Add tools/signet-sdk to extract ABIs from npm package - Store extracted ABIs in apps/explorer/priv/contracts_abi/signet/ - Create Indexer.Fetcher.Signet.Abi module for event topic computation - Update EventParser to use correct SDK-defined event signatures - Fix Output struct decoding: (token, amount, recipient, chainId) - Add chainId to formatted JSON outputs for cross-chain tracking Key changes: - Event signatures now match @signet-sh/sdk v0.3.0 - Output struct properly decoded with all 4 fields - Sweep event correctly parses indexed topics for recipient/token To update ABIs when SDK changes: cd tools/signet-sdk && npm install && npm run extract --- .gitignore | 3 + .../contracts_abi/signet/bundle_helper.json | 184 ++++++ .../contracts_abi/signet/events_index.json | 462 +++++++++++++++ .../contracts_abi/signet/host_orders.json | 280 +++++++++ .../priv/contracts_abi/signet/passage.json | 504 ++++++++++++++++ .../priv/contracts_abi/signet/permit2.json | 23 + .../contracts_abi/signet/rollup_orders.json | 540 ++++++++++++++++++ .../contracts_abi/signet/rollup_passage.json | 280 +++++++++ .../priv/contracts_abi/signet/transactor.json | 294 ++++++++++ .../priv/contracts_abi/signet/weth.json | 60 ++ .../priv/contracts_abi/signet/zenith.json | 291 ++++++++++ .../indexer/lib/indexer/fetcher/signet/abi.ex | 132 +++++ .../indexer/fetcher/signet/event_parser.ex | 150 +++-- .../indexer/fetcher/signet/orders_fetcher.ex | 29 +- tools/signet-sdk/README.md | 74 +++ tools/signet-sdk/extract-abis.mjs | 67 +++ tools/signet-sdk/package-lock.json | 244 ++++++++ tools/signet-sdk/package.json | 13 + 18 files changed, 3553 insertions(+), 77 deletions(-) create mode 100644 apps/explorer/priv/contracts_abi/signet/bundle_helper.json create mode 100644 apps/explorer/priv/contracts_abi/signet/events_index.json create mode 100644 apps/explorer/priv/contracts_abi/signet/host_orders.json create mode 100644 apps/explorer/priv/contracts_abi/signet/passage.json create mode 100644 apps/explorer/priv/contracts_abi/signet/permit2.json create mode 100644 apps/explorer/priv/contracts_abi/signet/rollup_orders.json create mode 100644 apps/explorer/priv/contracts_abi/signet/rollup_passage.json create mode 100644 apps/explorer/priv/contracts_abi/signet/transactor.json create mode 100644 apps/explorer/priv/contracts_abi/signet/weth.json create mode 100644 apps/explorer/priv/contracts_abi/signet/zenith.json create mode 100644 apps/indexer/lib/indexer/fetcher/signet/abi.ex create mode 100644 tools/signet-sdk/README.md create mode 100644 tools/signet-sdk/extract-abis.mjs create mode 100644 tools/signet-sdk/package-lock.json create mode 100644 tools/signet-sdk/package.json diff --git a/.gitignore b/.gitignore index f3545add61b3..5357ad0ed7c5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ npm-debug.log # Static artifacts /apps/**/node_modules +# Tools dependencies +/tools/**/node_modules + # Since we are building assets from assets/, # we ignore priv/static. You may want to comment # this depending on your deployment strategy. diff --git a/apps/explorer/priv/contracts_abi/signet/bundle_helper.json b/apps/explorer/priv/contracts_abi/signet/bundle_helper.json new file mode 100644 index 000000000000..7301cb81ed94 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/bundle_helper.json @@ -0,0 +1,184 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_zenith", + "type": "address", + "internalType": "address" + }, + { + "name": "_orders", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "orders", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract HostOrders" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "submit", + "inputs": [ + { + "name": "fills", + "type": "tuple[]", + "internalType": "struct BundleHelper.FillPermit2[]", + "components": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "permit2", + "type": "tuple", + "internalType": "struct UsesPermit2.Permit2Batch", + "components": [ + { + "name": "permit", + "type": "tuple", + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "components": [ + { + "name": "permitted", + "type": "tuple[]", + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ] + }, + { + "name": "header", + "type": "tuple", + "internalType": "struct Zenith.BlockHeader", + "components": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "hostBlockNumber", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gasLimit", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rewardAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "blockDataHash", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "v", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "r", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "zenith", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract Zenith" + } + ], + "stateMutability": "view" + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/events_index.json b/apps/explorer/priv/contracts_abi/signet/events_index.json new file mode 100644 index 000000000000..3cba039af03a --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/events_index.json @@ -0,0 +1,462 @@ +[ + { + "contract": "rollup_orders", + "name": "Filled", + "signature": "Filled(tuple[])", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ] + }, + { + "contract": "rollup_orders", + "name": "Order", + "signature": "Order(uint256,tuple[],tuple[])", + "inputs": [ + { + "name": "deadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "inputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Input[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "outputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ] + }, + { + "contract": "rollup_orders", + "name": "Sweep", + "signature": "Sweep(address,address,uint256)", + "inputs": [ + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "host_orders", + "name": "Filled", + "signature": "Filled(tuple[])", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ] + }, + { + "contract": "passage", + "name": "Enter", + "signature": "Enter(uint256,address,uint256)", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "passage", + "name": "EnterConfigured", + "signature": "EnterConfigured(address,bool)", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "canEnter", + "type": "bool", + "indexed": true, + "internalType": "bool" + } + ] + }, + { + "contract": "passage", + "name": "EnterToken", + "signature": "EnterToken(uint256,address,address,uint256)", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "passage", + "name": "Withdrawal", + "signature": "Withdrawal(address,address,uint256)", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "rollup_passage", + "name": "Exit", + "signature": "Exit(address,uint256)", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "rollup_passage", + "name": "ExitToken", + "signature": "ExitToken(address,address,uint256)", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "weth", + "name": "Deposit", + "signature": "Deposit(address,uint256)", + "inputs": [ + { + "name": "dst", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "wad", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "weth", + "name": "Withdrawal", + "signature": "Withdrawal(address,uint256)", + "inputs": [ + { + "name": "src", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "wad", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "zenith", + "name": "BlockSubmitted", + "signature": "BlockSubmitted(address,uint256,uint256,address,bytes32)", + "inputs": [ + { + "name": "sequencer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "gasLimit", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "rewardAddress", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "blockDataHash", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ] + }, + { + "contract": "zenith", + "name": "SequencerSet", + "signature": "SequencerSet(address,bool)", + "inputs": [ + { + "name": "sequencer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "permissioned", + "type": "bool", + "indexed": true, + "internalType": "bool" + } + ] + }, + { + "contract": "transactor", + "name": "GasConfigured", + "signature": "GasConfigured(uint256,uint256)", + "inputs": [ + { + "name": "perBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "perTransact", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + }, + { + "contract": "transactor", + "name": "Transact", + "signature": "Transact(uint256,address,address,bytes,uint256,uint256,uint256)", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "gas", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "maxFeePerGas", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ] + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/host_orders.json b/apps/explorer/priv/contracts_abi/signet/host_orders.json new file mode 100644 index 000000000000..0ea5b976ed86 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/host_orders.json @@ -0,0 +1,280 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_permit2", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "fill", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "fillPermit2", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "permit2", + "type": "tuple", + "internalType": "struct UsesPermit2.Permit2Batch", + "components": [ + { + "name": "permit", + "type": "tuple", + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "components": [ + { + "name": "permitted", + "type": "tuple[]", + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "outputWitness", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "outputs": [ + { + "name": "_witness", + "type": "tuple", + "internalType": "struct UsesPermit2.Witness", + "components": [ + { + "name": "witnessHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "witnessTypeString", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "event", + "name": "Filled", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "LengthMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "OutputMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/passage.json b/apps/explorer/priv/contracts_abi/signet/passage.json new file mode 100644 index 000000000000..7fec8c89792a --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/passage.json @@ -0,0 +1,504 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_defaultRollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_tokenAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "initialEnterTokens", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "_permit2", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "fallback", + "stateMutability": "payable" + }, + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "canEnter", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "configureEnter", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "_canEnter", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "defaultRollupChainId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "enter", + "inputs": [ + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "enter", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "enterToken", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "enterToken", + "inputs": [ + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "enterTokenPermit2", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "permit2", + "type": "tuple", + "internalType": "struct UsesPermit2.Permit2", + "components": [ + { + "name": "permit", + "type": "tuple", + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "components": [ + { + "name": "permitted", + "type": "tuple", + "internalType": "struct ISignatureTransfer.TokenPermissions", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "enterWitness", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "_witness", + "type": "tuple", + "internalType": "struct UsesPermit2.Witness", + "components": [ + { + "name": "witnessHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "witnessTypeString", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "exitWitness", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "_witness", + "type": "tuple", + "internalType": "struct UsesPermit2.Witness", + "components": [ + { + "name": "witnessHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "witnessTypeString", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "tokenAdmin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Enter", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EnterConfigured", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "canEnter", + "type": "bool", + "indexed": true, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EnterToken", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Withdrawal", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "DisallowedEnter", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "OnlyTokenAdmin", + "inputs": [] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/permit2.json b/apps/explorer/priv/contracts_abi/signet/permit2.json new file mode 100644 index 000000000000..b00239edbace --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/permit2.json @@ -0,0 +1,23 @@ +[ + { + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "wordPosition", + "type": "uint256" + } + ], + "name": "nonceBitmap", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/rollup_orders.json b/apps/explorer/priv/contracts_abi/signet/rollup_orders.json new file mode 100644 index 000000000000..097e8469094b --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/rollup_orders.json @@ -0,0 +1,540 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_permit2", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "fill", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "fillPermit2", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "permit2", + "type": "tuple", + "internalType": "struct UsesPermit2.Permit2Batch", + "components": [ + { + "name": "permit", + "type": "tuple", + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "components": [ + { + "name": "permitted", + "type": "tuple[]", + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initiate", + "inputs": [ + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "inputs", + "type": "tuple[]", + "internalType": "struct IOrders.Input[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "initiatePermit2", + "inputs": [ + { + "name": "tokenRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "permit2", + "type": "tuple", + "internalType": "struct UsesPermit2.Permit2Batch", + "components": [ + { + "name": "permit", + "type": "tuple", + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "components": [ + { + "name": "permitted", + "type": "tuple[]", + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "outputWitness", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "outputs": [ + { + "name": "_witness", + "type": "tuple", + "internalType": "struct UsesPermit2.Witness", + "components": [ + { + "name": "witnessHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "witnessTypeString", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "sweep", + "inputs": [ + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Filled", + "inputs": [ + { + "name": "outputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Order", + "inputs": [ + { + "name": "deadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "inputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Input[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "outputs", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IOrders.Output[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "chainId", + "type": "uint32", + "internalType": "uint32" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Sweep", + "inputs": [ + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "LengthMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "OrderExpired", + "inputs": [] + }, + { + "type": "error", + "name": "OutputMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/rollup_passage.json b/apps/explorer/priv/contracts_abi/signet/rollup_passage.json new file mode 100644 index 000000000000..905279436053 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/rollup_passage.json @@ -0,0 +1,280 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_permit2", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "fallback", + "stateMutability": "payable" + }, + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "enterWitness", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rollupRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "_witness", + "type": "tuple", + "internalType": "struct UsesPermit2.Witness", + "components": [ + { + "name": "witnessHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "witnessTypeString", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "exit", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "exitToken", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "exitTokenPermit2", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "permit2", + "type": "tuple", + "internalType": "struct UsesPermit2.Permit2", + "components": [ + { + "name": "permit", + "type": "tuple", + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "components": [ + { + "name": "permitted", + "type": "tuple", + "internalType": "struct ISignatureTransfer.TokenPermissions", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "exitWitness", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "_witness", + "type": "tuple", + "internalType": "struct UsesPermit2.Witness", + "components": [ + { + "name": "witnessHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "witnessTypeString", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "event", + "name": "Exit", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ExitToken", + "inputs": [ + { + "name": "hostRecipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/transactor.json b/apps/explorer/priv/contracts_abi/signet/transactor.json new file mode 100644 index 000000000000..7b6bfa8b1609 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/transactor.json @@ -0,0 +1,294 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_defaultRollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_gasAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "_passage", + "type": "address", + "internalType": "contract Passage" + }, + { + "name": "_perBlockGasLimit", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_perTransactGasLimit", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "configureGas", + "inputs": [ + { + "name": "perBlock", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "perTransact", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "defaultRollupChainId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "gasAdmin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "passage", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract Passage" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "perBlockGasLimit", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "perTransactGasLimit", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transact", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gas", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxFeePerGas", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "transact", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gas", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxFeePerGas", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "transactGasUsed", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "GasConfigured", + "inputs": [ + { + "name": "perBlock", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "perTransact", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transact", + "inputs": [ + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "gas", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "maxFeePerGas", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "OnlyGasAdmin", + "inputs": [] + }, + { + "type": "error", + "name": "PerBlockTransactGasLimit", + "inputs": [] + }, + { + "type": "error", + "name": "PerTransactGasLimit", + "inputs": [] + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/weth.json b/apps/explorer/priv/contracts_abi/signet/weth.json new file mode 100644 index 000000000000..e6c8ff5adf85 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/weth.json @@ -0,0 +1,60 @@ +[ + { + "type": "function", + "name": "deposit", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "wad", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Deposit", + "inputs": [ + { + "name": "dst", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "wad", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Withdrawal", + "inputs": [ + { + "name": "src", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "wad", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/zenith.json b/apps/explorer/priv/contracts_abi/signet/zenith.json new file mode 100644 index 000000000000..c75b02cb3633 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/signet/zenith.json @@ -0,0 +1,291 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_sequencerAdmin", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addSequencer", + "inputs": [ + { + "name": "sequencer", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "blockCommitment", + "inputs": [ + { + "name": "header", + "type": "tuple", + "internalType": "struct Zenith.BlockHeader", + "components": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "hostBlockNumber", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gasLimit", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rewardAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "blockDataHash", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ], + "outputs": [ + { + "name": "commit", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "deployBlockNumber", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSequencer", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lastSubmittedAtBlock", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeSequencer", + "inputs": [ + { + "name": "sequencer", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "sequencerAdmin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "submitBlock", + "inputs": [ + { + "name": "header", + "type": "tuple", + "internalType": "struct Zenith.BlockHeader", + "components": [ + { + "name": "rollupChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "hostBlockNumber", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gasLimit", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rewardAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "blockDataHash", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "v", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "r", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "BlockSubmitted", + "inputs": [ + { + "name": "sequencer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "rollupChainId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "gasLimit", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "rewardAddress", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "blockDataHash", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SequencerSet", + "inputs": [ + { + "name": "sequencer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "permissioned", + "type": "bool", + "indexed": true, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "BadSignature", + "inputs": [ + { + "name": "derivedSequencer", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "IncorrectHostBlock", + "inputs": [] + }, + { + "type": "error", + "name": "OneRollupBlockPerHostBlock", + "inputs": [] + }, + { + "type": "error", + "name": "OnlySequencerAdmin", + "inputs": [] + } +] \ No newline at end of file diff --git a/apps/indexer/lib/indexer/fetcher/signet/abi.ex b/apps/indexer/lib/indexer/fetcher/signet/abi.ex new file mode 100644 index 000000000000..32b84ab8a080 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signet/abi.ex @@ -0,0 +1,132 @@ +defmodule Indexer.Fetcher.Signet.Abi do + @moduledoc """ + ABI definitions for Signet contracts. + + ABIs are sourced from @signet-sh/sdk npm package and stored as JSON files + in apps/explorer/priv/contracts_abi/signet/. + + To update ABIs: + cd tools/signet-sdk && npm run extract + + ## Event Signatures (from SDK) + + RollupOrders contract: + - Order(uint256 deadline, (address token, uint256 amount)[] inputs, (address token, uint256 amount, address recipient, uint32 chainId)[] outputs) + - Filled((address token, uint256 amount, address recipient, uint32 chainId)[] outputs) + - Sweep(address indexed recipient, address indexed token, uint256 amount) + + HostOrders contract: + - Filled((address token, uint256 amount, address recipient, uint32 chainId)[] outputs) + """ + + require Logger + + # Compute event topic hashes at compile time + # These match the event signatures from @signet-sh/sdk rollupOrdersAbi + + # Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) + @order_event_signature "Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[])" + + # Filled((address,uint256,address,uint32)[]) + @filled_event_signature "Filled((address,uint256,address,uint32)[])" + + # Sweep(address,address,uint256) - recipient and token are indexed + @sweep_event_signature "Sweep(address,address,uint256)" + + @doc """ + Returns the keccak256 topic hash for the Order event. + """ + @spec order_event_topic() :: binary() + def order_event_topic do + "0x" <> Base.encode16(ExKeccak.hash_256(@order_event_signature), case: :lower) + end + + @doc """ + Returns the keccak256 topic hash for the Filled event. + """ + @spec filled_event_topic() :: binary() + def filled_event_topic do + "0x" <> Base.encode16(ExKeccak.hash_256(@filled_event_signature), case: :lower) + end + + @doc """ + Returns the keccak256 topic hash for the Sweep event. + """ + @spec sweep_event_topic() :: binary() + def sweep_event_topic do + "0x" <> Base.encode16(ExKeccak.hash_256(@sweep_event_signature), case: :lower) + end + + @doc """ + Returns all event topics for the RollupOrders contract. + """ + @spec rollup_orders_event_topics() :: [binary()] + def rollup_orders_event_topics do + [order_event_topic(), filled_event_topic(), sweep_event_topic()] + end + + @doc """ + Returns all event topics for the HostOrders contract. + Only the Filled event is relevant from the host chain. + """ + @spec host_orders_event_topics() :: [binary()] + def host_orders_event_topics do + [filled_event_topic()] + end + + @doc """ + Load a Signet contract ABI from the priv directory. + + ## Examples + + iex> Abi.load_abi("rollup_orders") + {:ok, [...]} + + iex> Abi.load_abi("nonexistent") + {:error, :not_found} + """ + @spec load_abi(String.t()) :: {:ok, list()} | {:error, atom()} + def load_abi(contract_name) do + path = abi_path(contract_name) + + case File.read(path) do + {:ok, content} -> + {:ok, Jason.decode!(content)} + + {:error, :enoent} -> + Logger.warning("Signet ABI not found: #{path}") + {:error, :not_found} + + {:error, reason} -> + Logger.error("Failed to load Signet ABI #{contract_name}: #{inspect(reason)}") + {:error, reason} + end + end + + @doc """ + Get the file path for a Signet contract ABI. + """ + @spec abi_path(String.t()) :: String.t() + def abi_path(contract_name) do + :explorer + |> Application.app_dir("priv/contracts_abi/signet/#{contract_name}.json") + end + + @doc """ + Returns the event signature string for the Order event. + """ + @spec order_event_signature() :: String.t() + def order_event_signature, do: @order_event_signature + + @doc """ + Returns the event signature string for the Filled event. + """ + @spec filled_event_signature() :: String.t() + def filled_event_signature, do: @filled_event_signature + + @doc """ + Returns the event signature string for the Sweep event. + """ + @spec sweep_event_signature() :: String.t() + def sweep_event_signature, do: @sweep_event_signature +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index 1e2b5bcac54d..e05a9c205349 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -2,36 +2,38 @@ defmodule Indexer.Fetcher.Signet.EventParser do @moduledoc """ Parses Signet Order and Filled events from transaction logs. - Handles ABI decoding for: - - Order(uint256 deadline, Input[] inputs, Output[] outputs) - - Filled(Output[] outputs) - - Sweep(address recipient, address token, uint256 amount) + Event signatures and ABI types are sourced from @signet-sh/sdk. + See `Indexer.Fetcher.Signet.Abi` for topic hash computation. + ## Event Structures (from @signet-sh/sdk) + + ### Order Event + ``` + Order(uint256 deadline, Input[] inputs, Output[] outputs) + ``` Where: - Input = (address token, uint256 amount) - - Output = (address recipient, address token, uint256 amount) + - Output = (address token, uint256 amount, address recipient, uint32 chainId) + + ### Filled Event + ``` + Filled(Output[] outputs) + ``` + + ### Sweep Event + ``` + Sweep(address indexed recipient, address indexed token, uint256 amount) + ``` + + ## Cross-Chain Correlation + + Orders are correlated with fills across chains using the `outputs_witness_hash`, + computed as: keccak256(concat(keccak256(abi.encode(output)) for each output)) """ require Logger - # Event topic hashes - @order_event_topic "0x" <> - Base.encode16( - ExKeccak.hash_256("Order(uint256,(address,uint256)[],(address,address,uint256)[])"), - case: :lower - ) - - @filled_event_topic "0x" <> - Base.encode16( - ExKeccak.hash_256("Filled((address,address,uint256)[])"), - case: :lower - ) - - @sweep_event_topic "0x" <> - Base.encode16( - ExKeccak.hash_256("Sweep(address,address,uint256)"), - case: :lower - ) + alias Indexer.Fetcher.Signet.Abi @doc """ Parse logs from the RollupOrders contract. @@ -41,12 +43,16 @@ defmodule Indexer.Fetcher.Signet.EventParser do """ @spec parse_rollup_logs([map()]) :: {:ok, {[map()], [map()]}} def parse_rollup_logs(logs) when is_list(logs) do + order_topic = Abi.order_event_topic() + filled_topic = Abi.filled_event_topic() + sweep_topic = Abi.sweep_event_topic() + {orders, fills, sweeps} = Enum.reduce(logs, {[], [], []}, fn log, {orders_acc, fills_acc, sweeps_acc} -> topic = get_topic(log, 0) cond do - topic == @order_event_topic -> + topic == order_topic -> case parse_order_event(log) do {:ok, order} -> {[order | orders_acc], fills_acc, sweeps_acc} {:error, reason} -> @@ -54,7 +60,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do {orders_acc, fills_acc, sweeps_acc} end - topic == @filled_event_topic -> + topic == filled_topic -> case parse_filled_event(log) do {:ok, fill} -> {orders_acc, [fill | fills_acc], sweeps_acc} {:error, reason} -> @@ -62,7 +68,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do {orders_acc, fills_acc, sweeps_acc} end - topic == @sweep_event_topic -> + topic == sweep_topic -> case parse_sweep_event(log) do {:ok, sweep} -> {orders_acc, fills_acc, [sweep | sweeps_acc]} {:error, reason} -> @@ -88,9 +94,11 @@ defmodule Indexer.Fetcher.Signet.EventParser do """ @spec parse_host_filled_logs([map()]) :: {:ok, [map()]} def parse_host_filled_logs(logs) when is_list(logs) do + filled_topic = Abi.filled_event_topic() + fills = logs - |> Enum.filter(fn log -> get_topic(log, 0) == @filled_event_topic end) + |> Enum.filter(fn log -> get_topic(log, 0) == filled_topic end) |> Enum.map(&parse_filled_event/1) |> Enum.filter(fn {:ok, _} -> true @@ -107,19 +115,28 @@ defmodule Indexer.Fetcher.Signet.EventParser do Compute the outputs_witness_hash for a list of outputs. The hash is computed as: keccak256(concat(keccak256(abi_encode(output)) for output in outputs)) + + Output struct (from @signet-sh/sdk): + - token: address + - amount: uint256 + - recipient: address + - chainId: uint32 """ - @spec compute_outputs_witness_hash([{binary(), binary(), non_neg_integer()}]) :: binary() + @spec compute_outputs_witness_hash([{binary(), non_neg_integer(), binary(), non_neg_integer()}]) :: binary() def compute_outputs_witness_hash(outputs) do output_hashes = outputs - |> Enum.map(fn {recipient, token, amount} -> - # ABI-encode each output as (address, address, uint256) + |> Enum.map(fn {token, amount, recipient, chain_id} -> + # ABI-encode each output as (address token, uint256 amount, address recipient, uint32 chainId) + # Padded to 32 bytes each encoded = - <<0::size(96)>> <> - normalize_address(recipient) <> <<0::size(96)>> <> normalize_address(token) <> - <> + <> <> + <<0::size(96)>> <> + normalize_address(recipient) <> + <<0::size(224)>> <> + <> ExKeccak.hash_256(encoded) end) @@ -129,6 +146,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do end # Parse Order event + # Order(uint256 deadline, Input[] inputs, Output[] outputs) defp parse_order_event(log) do data = get_log_data(log) @@ -152,6 +170,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do end # Parse Filled event + # Filled(Output[] outputs) defp parse_filled_event(log) do data = get_log_data(log) @@ -171,10 +190,16 @@ defmodule Indexer.Fetcher.Signet.EventParser do end # Parse Sweep event + # Sweep(address indexed recipient, address indexed token, uint256 amount) + # Note: recipient and token are indexed (in topics), amount is in data defp parse_sweep_event(log) do data = get_log_data(log) - with {:ok, {recipient, token, amount}} <- decode_sweep_data(data) do + with {:ok, amount} <- decode_sweep_data(data) do + # recipient is in topic[1], token is in topic[2] + recipient = get_indexed_address(log, 1) + token = get_indexed_address(log, 2) + sweep = %{ transaction_hash: get_transaction_hash(log), recipient: recipient, @@ -189,18 +214,16 @@ defmodule Indexer.Fetcher.Signet.EventParser do # Decode Order event data # Order(uint256 deadline, Input[] inputs, Output[] outputs) # Input = (address token, uint256 amount) - # Output = (address recipient, address token, uint256 amount) + # Output = (address token, uint256 amount, address recipient, uint32 chainId) defp decode_order_data(data) when is_binary(data) do try do - # ABI decode: uint256, (address,uint256)[], (address,address,uint256)[] - # For dynamic arrays, we have offsets first, then the actual data - + # ABI decode: uint256, dynamic array offset, dynamic array offset <> = data - # Parse inputs array + # Parse inputs array - offset is from start of data (after first 32 bytes) inputs_data = binary_part(rest, inputs_offset - 96, byte_size(rest) - inputs_offset + 96) inputs = decode_input_array(inputs_data) @@ -235,16 +258,11 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_filled_data(_), do: {:error, :invalid_data} # Decode Sweep event data - # Sweep(address recipient, address token, uint256 amount) + # Only amount is in data (recipient and token are indexed) defp decode_sweep_data(data) when is_binary(data) do try do - <<_padding1::binary-size(12), - recipient::binary-size(20), - _padding2::binary-size(12), - token::binary-size(20), - amount::unsigned-big-integer-size(256)>> = data - - {:ok, {recipient, token, amount}} + <> = data + {:ok, amount} rescue e -> Logger.error("Error decoding Sweep data: #{inspect(e)}") @@ -255,6 +273,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_sweep_data(_), do: {:error, :invalid_data} # Decode array of Input tuples + # Input = (address token, uint256 amount) defp decode_input_array(<>) do decode_inputs(rest, length, []) end @@ -270,6 +289,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do end # Decode array of Output tuples + # Output = (address token, uint256 amount, address recipient, uint32 chainId) defp decode_output_array(<>) do decode_outputs(rest, length, []) end @@ -277,12 +297,15 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_outputs(_data, 0, acc), do: Enum.reverse(acc) defp decode_outputs(<<_padding1::binary-size(12), - recipient::binary-size(20), - _padding2::binary-size(12), token::binary-size(20), amount::unsigned-big-integer-size(256), + _padding2::binary-size(12), + recipient::binary-size(20), + _padding3::binary-size(28), + chain_id::unsigned-big-integer-size(32), rest::binary>>, count, acc) do - output = {recipient, token, amount} + # Output struct order: token, amount, recipient, chainId + output = {token, amount, recipient, chain_id} decode_outputs(rest, count - 1, [output | acc]) end @@ -316,12 +339,14 @@ defmodule Indexer.Fetcher.Signet.EventParser do end # Format outputs for JSON storage + # Output = (token, amount, recipient, chainId) defp format_outputs(outputs) do - Enum.map(outputs, fn {recipient, token, amount} -> + Enum.map(outputs, fn {token, amount, recipient, chain_id} -> %{ - "recipient" => format_address(recipient), "token" => format_address(token), - "amount" => Integer.to_string(amount) + "amount" => Integer.to_string(amount), + "recipient" => format_address(recipient), + "chainId" => chain_id } end) end @@ -341,6 +366,25 @@ defmodule Indexer.Fetcher.Signet.EventParser do Enum.at(topics, index) end + # Get an indexed address from topics (topics contain 32-byte padded addresses) + defp get_indexed_address(log, topic_index) do + topic = get_topic(log, topic_index) + + case topic do + "0x" <> hex -> + # Take last 40 chars (20 bytes) of the 64-char hex string + address_hex = String.slice(hex, -40, 40) + Base.decode16!(address_hex, case: :mixed) + + bytes when is_binary(bytes) and byte_size(bytes) == 32 -> + # Take last 20 bytes + binary_part(bytes, 12, 20) + + _ -> + nil + end + end + defp get_log_data(log) do data = Map.get(log, "data") || Map.get(log, :data) || "" diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 63568d99d03f..550e7094941d 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -46,32 +46,13 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do alias Explorer.Chain alias Explorer.Chain.Signet.{Order, Fill} alias Indexer.BufferedTask - alias Indexer.Fetcher.Signet.{EventParser, ReorgHandler} + alias Indexer.Fetcher.Signet.{Abi, EventParser, ReorgHandler} alias Indexer.Helper, as: IndexerHelper @behaviour BufferedTask - # Event topic hashes (keccak256 of event signatures) - # Order(uint256,tuple[],tuple[]) - @order_event_topic "0x" <> - Base.encode16( - ExKeccak.hash_256("Order(uint256,(address,uint256)[],(address,address,uint256)[])"), - case: :lower - ) - - # Filled(tuple[]) - @filled_event_topic "0x" <> - Base.encode16( - ExKeccak.hash_256("Filled((address,address,uint256)[])"), - case: :lower - ) - - # Sweep(address,address,uint256) - @sweep_event_topic "0x" <> - Base.encode16( - ExKeccak.hash_256("Sweep(address,address,uint256)"), - case: :lower - ) + # Event topic hashes are computed from @signet-sh/sdk ABIs + # See Indexer.Fetcher.Signet.Abi for event signature definitions # 250ms interval between processing buffered entries @idle_interval 250 @@ -328,7 +309,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do config.host_orders_address, start_block, end_block, - [@filled_event_topic] + Abi.host_orders_event_topics() ), {:ok, fills} <- EventParser.parse_host_filled_logs(logs), :ok <- import_fills(fills, :host) do @@ -370,7 +351,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do end defp fetch_logs(json_rpc_named_arguments, contract_address, from_block, to_block, topics \\ nil) do - topics = topics || [@order_event_topic, @filled_event_topic, @sweep_event_topic] + topics = topics || Abi.rollup_orders_event_topics() request = %{ id: 1, diff --git a/tools/signet-sdk/README.md b/tools/signet-sdk/README.md new file mode 100644 index 000000000000..416bf8a8ffcf --- /dev/null +++ b/tools/signet-sdk/README.md @@ -0,0 +1,74 @@ +# Signet SDK ABI Extractor + +This tool extracts ABI definitions from the `@signet-sh/sdk` npm package for use in the Elixir-based Blockscout indexer. + +## Overview + +Blockscout is an Elixir application, but the Signet protocol's canonical ABI definitions are maintained in the TypeScript SDK (`@signet-sh/sdk`). This tool bridges that gap by: + +1. Installing the SDK as an npm dependency +2. Extracting ABIs as JSON files +3. Storing them in `apps/explorer/priv/contracts_abi/signet/` + +The Elixir indexer then loads these JSON files via `Indexer.Fetcher.Signet.Abi`. + +## Usage + +### Initial Setup + +```bash +cd tools/signet-sdk +npm install +npm run extract +``` + +### Updating ABIs + +When the SDK is updated: + +1. Update the version in `package.json` +2. Run: + ```bash + npm install + npm run extract + ``` +3. Commit the updated JSON files in `apps/explorer/priv/contracts_abi/signet/` + +## Extracted ABIs + +The following ABIs are extracted from `@signet-sh/sdk`: + +| File | Contract | Description | +|------|----------|-------------| +| `rollup_orders.json` | RollupOrders | L2 order creation and fills | +| `host_orders.json` | HostOrders | L1 fills | +| `passage.json` | Passage | L1→L2 bridging | +| `rollup_passage.json` | RollupPassage | L2→L1 bridging | +| `permit2.json` | Permit2 | Gasless token approvals | +| `weth.json` | WETH | Wrapped ETH | +| `zenith.json` | Zenith | Block submission | +| `transactor.json` | Transactor | Cross-chain transactions | +| `bundle_helper.json` | BundleHelper | Bundle utilities | +| `events_index.json` | — | Index of all events | + +## Event Signatures + +Key events tracked by the indexer (from `rollup_orders.json`): + +- **Order**: `Order(uint256 deadline, (address,uint256)[] inputs, (address,uint256,address,uint32)[] outputs)` +- **Filled**: `Filled((address,uint256,address,uint32)[] outputs)` +- **Sweep**: `Sweep(address indexed recipient, address indexed token, uint256 amount)` + +## Architecture + +``` +@signet-sh/sdk (npm) + ↓ +tools/signet-sdk/extract-abis.mjs + ↓ +apps/explorer/priv/contracts_abi/signet/*.json + ↓ +Indexer.Fetcher.Signet.Abi (Elixir module) + ↓ +Indexer.Fetcher.Signet.EventParser +``` diff --git a/tools/signet-sdk/extract-abis.mjs b/tools/signet-sdk/extract-abis.mjs new file mode 100644 index 000000000000..b87b1d12926e --- /dev/null +++ b/tools/signet-sdk/extract-abis.mjs @@ -0,0 +1,67 @@ +/** + * Extract ABIs from @signet-sh/sdk and save as JSON files for use in Elixir. + * + * Run with: npm run extract + * Output: ../../apps/explorer/priv/contracts_abi/signet/ + */ + +import { writeFileSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +import { + rollupOrdersAbi, + hostOrdersAbi, + passageAbi, + rollupPassageAbi, + permit2Abi, + wethAbi, + zenithAbi, + transactorAbi, + bundleHelperAbi, +} from '@signet-sh/sdk'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OUTPUT_DIR = join(__dirname, '../../apps/explorer/priv/contracts_abi/signet'); + +// Ensure output directory exists +mkdirSync(OUTPUT_DIR, { recursive: true }); + +const abis = { + 'rollup_orders': rollupOrdersAbi, + 'host_orders': hostOrdersAbi, + 'passage': passageAbi, + 'rollup_passage': rollupPassageAbi, + 'permit2': permit2Abi, + 'weth': wethAbi, + 'zenith': zenithAbi, + 'transactor': transactorAbi, + 'bundle_helper': bundleHelperAbi, +}; + +for (const [name, abi] of Object.entries(abis)) { + const outputPath = join(OUTPUT_DIR, `${name}.json`); + writeFileSync(outputPath, JSON.stringify(abi, null, 2)); + console.log(`Wrote ${outputPath}`); +} + +// Also create a combined file with event signatures for quick reference +const events = []; +for (const [name, abi] of Object.entries(abis)) { + for (const item of abi) { + if (item.type === 'event') { + events.push({ + contract: name, + name: item.name, + signature: `${item.name}(${item.inputs.map(i => i.type).join(',')})`, + inputs: item.inputs, + }); + } + } +} + +const eventsPath = join(OUTPUT_DIR, 'events_index.json'); +writeFileSync(eventsPath, JSON.stringify(events, null, 2)); +console.log(`Wrote ${eventsPath}`); + +console.log('\nExtraction complete!'); diff --git a/tools/signet-sdk/package-lock.json b/tools/signet-sdk/package-lock.json new file mode 100644 index 000000000000..dcca622d6fb6 --- /dev/null +++ b/tools/signet-sdk/package-lock.json @@ -0,0 +1,244 @@ +{ + "name": "signet-sdk-extractor", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "signet-sdk-extractor", + "version": "1.0.0", + "dependencies": { + "@signet-sh/sdk": "^0.3.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@signet-sh/sdk": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@signet-sh/sdk/-/sdk-0.3.0.tgz", + "integrity": "sha512-mfMvNr5Y+0fDdRuispmwydcCC2UfZg5gkfDUox/nuDQ9OowM/ZHq0zUjQmcBbeGYFBi4aF0uCgi+X10g6y/CWw==", + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "viem": "^2.0.0" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT", + "peer": true + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peer": true, + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/ox": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.1.tgz", + "integrity": "sha512-uU0llpthaaw4UJoXlseCyBHmQ3bLrQmz9rRLIAUHqv46uHuae9SE+ukYBRIPVCnlEnHKuWjDUcDFHWx9gbGNoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.1.tgz", + "integrity": "sha512-c5YPQR/VueqoPG09Tp1JBw2iItKVRGVI0YkWekquRDZw0ciNBhO3muu2QjO9xFelOXh18q3d/kLbW83B2Oxf0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.12.1", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/tools/signet-sdk/package.json b/tools/signet-sdk/package.json new file mode 100644 index 000000000000..841b3013fb1a --- /dev/null +++ b/tools/signet-sdk/package.json @@ -0,0 +1,13 @@ +{ + "name": "signet-sdk-extractor", + "version": "1.0.0", + "private": true, + "description": "Extract ABIs from @signet-sh/sdk for use in Blockscout", + "type": "module", + "scripts": { + "extract": "node extract-abis.mjs" + }, + "dependencies": { + "@signet-sh/sdk": "^0.3.0" + } +} From 177b2b02cf46f21f402a5f9b96639706c53a10df Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 14:07:14 +0000 Subject: [PATCH 03/24] test: add unit tests for Signet EventParser and Abi modules - Fix Output struct documentation to match @signet-sh/sdk: (token, amount, recipient, chainId) not (recipient, token, amount) - Add comprehensive tests for Abi module: - Event topic hash computation and consistency - Event signatures format validation - ABI file loading from priv directory - Add comprehensive tests for EventParser module: - outputs_witness_hash determinism and correctness - Output field order verification (critical for cross-chain correlation) - Log parsing edge cases - Event topic matching with Abi module Part of Phase 1 completion for ENG-1876 --- .../lib/indexer/fetcher/signet/README.md | 2 +- .../test/indexer/fetcher/signet/abi_test.exs | 154 +++++++++++ .../fetcher/signet/event_parser_test.exs | 246 ++++++++++++++++++ 3 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 apps/indexer/test/indexer/fetcher/signet/abi_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs diff --git a/apps/indexer/lib/indexer/fetcher/signet/README.md b/apps/indexer/lib/indexer/fetcher/signet/README.md index 310f379fa4f3..f14a675db7fa 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/README.md +++ b/apps/indexer/lib/indexer/fetcher/signet/README.md @@ -26,7 +26,7 @@ The Signet protocol enables cross-chain orders between a rollup (L2) and its hos ## Data Structures **Input:** `(address token, uint256 amount)` -**Output:** `(address recipient, address token, uint256 amount)` +**Output:** `(address token, uint256 amount, address recipient, uint32 chainId)` ## Configuration diff --git a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs new file mode 100644 index 000000000000..c87a46220c9a --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs @@ -0,0 +1,154 @@ +defmodule Indexer.Fetcher.Signet.AbiTest do + @moduledoc """ + Unit tests for Indexer.Fetcher.Signet.Abi module. + + Tests verify event topic hash computation and ABI loading functionality. + """ + + use ExUnit.Case, async: true + + alias Indexer.Fetcher.Signet.Abi + + describe "event topic hashes" do + test "order_event_topic/0 returns valid keccak256 hash" do + topic = Abi.order_event_topic() + + # Should be a hex string starting with 0x and 64 hex chars (32 bytes) + assert String.starts_with?(topic, "0x") + assert String.length(topic) == 66 + + # Verify it's a valid hex string + "0x" <> hex = topic + assert {:ok, _} = Base.decode16(hex, case: :lower) + end + + test "filled_event_topic/0 returns valid keccak256 hash" do + topic = Abi.filled_event_topic() + + assert String.starts_with?(topic, "0x") + assert String.length(topic) == 66 + + "0x" <> hex = topic + assert {:ok, _} = Base.decode16(hex, case: :lower) + end + + test "sweep_event_topic/0 returns valid keccak256 hash" do + topic = Abi.sweep_event_topic() + + assert String.starts_with?(topic, "0x") + assert String.length(topic) == 66 + + "0x" <> hex = topic + assert {:ok, _} = Base.decode16(hex, case: :lower) + end + + test "event topics are different from each other" do + order_topic = Abi.order_event_topic() + filled_topic = Abi.filled_event_topic() + sweep_topic = Abi.sweep_event_topic() + + refute order_topic == filled_topic + refute order_topic == sweep_topic + refute filled_topic == sweep_topic + end + + test "event topics are consistent (deterministic)" do + # Topics should be the same on repeated calls + topic1 = Abi.order_event_topic() + topic2 = Abi.order_event_topic() + assert topic1 == topic2 + end + end + + describe "event signatures" do + test "order_event_signature/0 returns expected format" do + sig = Abi.order_event_signature() + + # Should follow format: Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) + assert String.starts_with?(sig, "Order(") + assert String.contains?(sig, "uint256") + assert String.contains?(sig, "address") + end + + test "filled_event_signature/0 returns expected format" do + sig = Abi.filled_event_signature() + + # Should follow format: Filled((address,uint256,address,uint32)[]) + assert String.starts_with?(sig, "Filled(") + assert String.contains?(sig, "uint32") + end + + test "sweep_event_signature/0 returns expected format" do + sig = Abi.sweep_event_signature() + + # Should follow format: Sweep(address,address,uint256) + assert String.starts_with?(sig, "Sweep(") + assert String.contains?(sig, "address") + end + end + + describe "rollup_orders_event_topics/0" do + test "returns list of three topics" do + topics = Abi.rollup_orders_event_topics() + + assert is_list(topics) + assert length(topics) == 3 + end + + test "includes all rollup event topics" do + topics = Abi.rollup_orders_event_topics() + + assert Abi.order_event_topic() in topics + assert Abi.filled_event_topic() in topics + assert Abi.sweep_event_topic() in topics + end + end + + describe "host_orders_event_topics/0" do + test "returns list with only filled topic" do + topics = Abi.host_orders_event_topics() + + assert is_list(topics) + assert length(topics) == 1 + assert Abi.filled_event_topic() in topics + end + end + + describe "abi_path/1" do + test "returns path for rollup_orders contract" do + path = Abi.abi_path("rollup_orders") + + assert String.contains?(path, "priv/contracts_abi/signet/rollup_orders.json") + end + + test "returns path for host_orders contract" do + path = Abi.abi_path("host_orders") + + assert String.contains?(path, "priv/contracts_abi/signet/host_orders.json") + end + end + + describe "load_abi/1" do + test "loads rollup_orders ABI successfully" do + result = Abi.load_abi("rollup_orders") + + assert {:ok, abi} = result + assert is_list(abi) + assert length(abi) > 0 + end + + test "loads host_orders ABI successfully" do + result = Abi.load_abi("host_orders") + + assert {:ok, abi} = result + assert is_list(abi) + assert length(abi) > 0 + end + + test "returns error for nonexistent contract" do + result = Abi.load_abi("nonexistent_contract") + + assert {:error, :not_found} = result + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs new file mode 100644 index 000000000000..78d9556989e7 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs @@ -0,0 +1,246 @@ +defmodule Indexer.Fetcher.Signet.EventParserTest do + @moduledoc """ + Unit tests for Indexer.Fetcher.Signet.EventParser module. + + Tests verify event parsing, Output struct field ordering, and + outputs_witness_hash computation for cross-chain order correlation. + + Output struct field order (per @signet-sh/sdk): + (address token, uint256 amount, address recipient, uint32 chainId) + """ + + use ExUnit.Case, async: true + + alias Indexer.Fetcher.Signet.{Abi, EventParser} + + # Test addresses (20 bytes each) + @test_token <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>> + @test_recipient <<21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40>> + @test_amount 1_000_000_000_000_000_000 # 1e18 + @test_chain_id 1 # Mainnet + + describe "compute_outputs_witness_hash/1" do + test "returns 32-byte hash for single output" do + outputs = [{@test_token, @test_amount, @test_recipient, @test_chain_id}] + + hash = EventParser.compute_outputs_witness_hash(outputs) + + assert byte_size(hash) == 32 + end + + test "returns deterministic hash for same outputs" do + outputs = [{@test_token, @test_amount, @test_recipient, @test_chain_id}] + + hash1 = EventParser.compute_outputs_witness_hash(outputs) + hash2 = EventParser.compute_outputs_witness_hash(outputs) + + assert hash1 == hash2 + end + + test "returns different hashes for different outputs" do + outputs1 = [{@test_token, @test_amount, @test_recipient, @test_chain_id}] + outputs2 = [{@test_token, @test_amount + 1, @test_recipient, @test_chain_id}] + + hash1 = EventParser.compute_outputs_witness_hash(outputs1) + hash2 = EventParser.compute_outputs_witness_hash(outputs2) + + refute hash1 == hash2 + end + + test "different chain_id produces different hash" do + outputs1 = [{@test_token, @test_amount, @test_recipient, 1}] + outputs2 = [{@test_token, @test_amount, @test_recipient, 42161}] # Arbitrum + + hash1 = EventParser.compute_outputs_witness_hash(outputs1) + hash2 = EventParser.compute_outputs_witness_hash(outputs2) + + refute hash1 == hash2 + end + + test "order of outputs matters" do + output1 = {@test_token, @test_amount, @test_recipient, @test_chain_id} + output2 = {@test_recipient, @test_amount * 2, @test_token, 42161} + + hash_ordered = EventParser.compute_outputs_witness_hash([output1, output2]) + hash_reversed = EventParser.compute_outputs_witness_hash([output2, output1]) + + refute hash_ordered == hash_reversed + end + + test "handles empty outputs list" do + hash = EventParser.compute_outputs_witness_hash([]) + + # Should still return a valid hash (hash of empty concat) + assert byte_size(hash) == 32 + end + + test "handles multiple outputs" do + outputs = [ + {@test_token, 100, @test_recipient, 1}, + {@test_recipient, 200, @test_token, 42161}, + {@test_token, 300, @test_recipient, 10} # Optimism + ] + + hash = EventParser.compute_outputs_witness_hash(outputs) + + assert byte_size(hash) == 32 + end + end + + describe "parse_rollup_logs/1" do + test "returns empty lists for empty logs" do + {:ok, {orders, fills}} = EventParser.parse_rollup_logs([]) + + assert orders == [] + assert fills == [] + end + + test "ignores logs with non-matching topics" do + logs = [ + %{ + "topics" => ["0x0000000000000000000000000000000000000000000000000000000000000000"], + "data" => "0x", + "blockNumber" => "0x1", + "transactionHash" => "0x" <> String.duplicate("ab", 32), + "logIndex" => "0x0" + } + ] + + {:ok, {orders, fills}} = EventParser.parse_rollup_logs(logs) + + assert orders == [] + assert fills == [] + end + end + + describe "parse_host_filled_logs/1" do + test "returns empty list for empty logs" do + {:ok, fills} = EventParser.parse_host_filled_logs([]) + + assert fills == [] + end + + test "ignores logs with non-matching topics" do + logs = [ + %{ + "topics" => ["0x0000000000000000000000000000000000000000000000000000000000000000"], + "data" => "0x", + "blockNumber" => "0x1", + "transactionHash" => "0x" <> String.duplicate("ab", 32), + "logIndex" => "0x0" + } + ] + + {:ok, fills} = EventParser.parse_host_filled_logs(logs) + + assert fills == [] + end + end + + describe "output struct field order verification" do + @tag :output_field_order + test "Output struct follows SDK order: (token, amount, recipient, chainId)" do + # Per @signet-sh/sdk, Output is defined as: + # struct Output { + # address token; + # uint256 amount; + # address recipient; + # uint32 chainId; + # } + # + # This is the correct field order used in the EventParser tuple: + # {token, amount, recipient, chain_id} + + token = @test_token + amount = @test_amount + recipient = @test_recipient + chain_id = @test_chain_id + + # The tuple format used in EventParser + output_tuple = {token, amount, recipient, chain_id} + + # Extract fields in the expected SDK order + {extracted_token, extracted_amount, extracted_recipient, extracted_chain_id} = output_tuple + + assert extracted_token == token + assert extracted_amount == amount + assert extracted_recipient == recipient + assert extracted_chain_id == chain_id + end + + test "compute_outputs_witness_hash uses correct Output encoding order" do + # When encoding for witness hash, the order must be: + # (token, amount, recipient, chainId) + # NOT the old incorrect order: (recipient, token, amount) + + # Two outputs with same data but different "interpretations" + # would produce different hashes if order is wrong + + token = @test_token + amount = 12345 + recipient = @test_recipient + chain_id = 42161 + + # Correct order: (token, amount, recipient, chainId) + correct_output = [{token, amount, recipient, chain_id}] + + # What the hash would be if we incorrectly swapped token/recipient + incorrect_output = [{recipient, amount, token, chain_id}] + + correct_hash = EventParser.compute_outputs_witness_hash(correct_output) + incorrect_hash = EventParser.compute_outputs_witness_hash(incorrect_output) + + # Hashes must be different - this validates the encoding uses correct order + refute correct_hash == incorrect_hash + end + end + + describe "log field parsing helpers" do + test "handles hex-encoded block numbers" do + # Test that block_number parsing works for hex strings + log = %{ + "blockNumber" => "0x10", # 16 in decimal + "topics" => [], + "data" => "0x" + } + + # The parser should correctly decode hex block numbers + # This is implicitly tested through parse_rollup_logs/1 + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + + test "handles integer block numbers" do + log = %{ + :block_number => 16, + :topics => [], + :data => "" + } + + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + end + + describe "event topic matching" do + test "Order event topic matches Abi module" do + order_topic = Abi.order_event_topic() + + # Verify the topic format + assert String.starts_with?(order_topic, "0x") + assert String.length(order_topic) == 66 + end + + test "Filled event topic matches Abi module" do + filled_topic = Abi.filled_event_topic() + + assert String.starts_with?(filled_topic, "0x") + assert String.length(filled_topic) == 66 + end + + test "Sweep event topic matches Abi module" do + sweep_topic = Abi.sweep_event_topic() + + assert String.starts_with?(sweep_topic, "0x") + assert String.length(sweep_topic) == 66 + end + end +end From 249f075b40954f41e2a0571fc35c4806df1d2773 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 14:15:12 +0000 Subject: [PATCH 04/24] refactor: remove outputs_witness_hash correlation logic Per James's architecture update: orders and fills cannot be correlated directly - only block-level coordination is possible. Changes: - Remove compute_outputs_witness_hash from EventParser - Change Order primary key to (transaction_hash, log_index) - Change Fill primary key to (chain_type, transaction_hash, log_index) - Update migration to use new composite primary keys - Update import runners for new primary keys - Remove correlation tests from EventParserTest - Update module docs to reflect independent indexing --- .../chain/import/runner/signet/fills.ex | 16 +-- .../chain/import/runner/signet/orders.ex | 16 +-- .../lib/explorer/chain/signet/fill.ex | 20 +-- .../lib/explorer/chain/signet/order.ex | 20 +-- .../20260216040000_create_signet_tables.exs | 21 +-- .../indexer/fetcher/signet/event_parser.ex | 54 +------- .../indexer/fetcher/signet/orders_fetcher.ex | 9 +- .../fetcher/signet/event_parser_test.exs | 126 +++++------------- 8 files changed, 83 insertions(+), 199 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex b/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex index 2bf2aaa531fe..958a8be0ef67 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex @@ -62,13 +62,17 @@ defmodule Explorer.Chain.Import.Runner.Signet.Fills do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce Fill ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, &{&1.outputs_witness_hash, &1.chain_type}) + # Sort by composite primary key: chain_type, transaction_hash, log_index + ordered_changes_list = + Enum.sort_by(changes_list, fn change -> + {change.chain_type, change.transaction_hash, change.log_index} + end) {:ok, inserted} = Import.insert_changes_list( repo, ordered_changes_list, - conflict_target: [:outputs_witness_hash, :chain_type], + conflict_target: [:chain_type, :transaction_hash, :log_index], on_conflict: on_conflict, for: Fill, returning: true, @@ -84,10 +88,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.Fills do f in Fill, update: [ set: [ - # Don't update composite primary key fields + # Don't update primary key fields (chain_type, transaction_hash, log_index) block_number: fragment("COALESCE(EXCLUDED.block_number, ?)", f.block_number), - transaction_hash: fragment("COALESCE(EXCLUDED.transaction_hash, ?)", f.transaction_hash), - log_index: fragment("COALESCE(EXCLUDED.log_index, ?)", f.log_index), outputs_json: fragment("COALESCE(EXCLUDED.outputs_json, ?)", f.outputs_json), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", f.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", f.updated_at) @@ -95,10 +97,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.Fills do ], where: fragment( - "(EXCLUDED.block_number, EXCLUDED.transaction_hash, EXCLUDED.log_index, EXCLUDED.outputs_json) IS DISTINCT FROM (?, ?, ?, ?)", + "(EXCLUDED.block_number, EXCLUDED.outputs_json) IS DISTINCT FROM (?, ?)", f.block_number, - f.transaction_hash, - f.log_index, f.outputs_json ) ) diff --git a/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex b/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex index de8c5eb7634b..b182137418c9 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex @@ -62,13 +62,17 @@ defmodule Explorer.Chain.Import.Runner.Signet.Orders do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce Order ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, & &1.outputs_witness_hash) + # Sort by composite primary key: transaction_hash, then log_index + ordered_changes_list = + Enum.sort_by(changes_list, fn change -> + {change.transaction_hash, change.log_index} + end) {:ok, inserted} = Import.insert_changes_list( repo, ordered_changes_list, - conflict_target: [:outputs_witness_hash], + conflict_target: [:transaction_hash, :log_index], on_conflict: on_conflict, for: Order, returning: true, @@ -84,11 +88,9 @@ defmodule Explorer.Chain.Import.Runner.Signet.Orders do o in Order, update: [ set: [ - # Don't update outputs_witness_hash as it's the primary key + # Don't update primary key fields (transaction_hash, log_index) deadline: fragment("COALESCE(EXCLUDED.deadline, ?)", o.deadline), block_number: fragment("COALESCE(EXCLUDED.block_number, ?)", o.block_number), - transaction_hash: fragment("COALESCE(EXCLUDED.transaction_hash, ?)", o.transaction_hash), - log_index: fragment("COALESCE(EXCLUDED.log_index, ?)", o.log_index), inputs_json: fragment("COALESCE(EXCLUDED.inputs_json, ?)", o.inputs_json), outputs_json: fragment("COALESCE(EXCLUDED.outputs_json, ?)", o.outputs_json), sweep_recipient: fragment("COALESCE(EXCLUDED.sweep_recipient, ?)", o.sweep_recipient), @@ -100,11 +102,9 @@ defmodule Explorer.Chain.Import.Runner.Signet.Orders do ], where: fragment( - "(EXCLUDED.deadline, EXCLUDED.block_number, EXCLUDED.transaction_hash, EXCLUDED.log_index, EXCLUDED.sweep_recipient, EXCLUDED.sweep_token, EXCLUDED.sweep_amount) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.deadline, EXCLUDED.block_number, EXCLUDED.sweep_recipient, EXCLUDED.sweep_token, EXCLUDED.sweep_amount) IS DISTINCT FROM (?, ?, ?, ?, ?)", o.deadline, o.block_number, - o.transaction_hash, - o.log_index, o.sweep_recipient, o.sweep_token, o.sweep_amount diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex index 7062617fe26a..1e71eb5a6a23 100644 --- a/apps/explorer/lib/explorer/chain/signet/fill.ex +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -2,6 +2,9 @@ defmodule Explorer.Chain.Signet.Fill do @moduledoc """ Models a Signet Filled event from RollupOrders or HostOrders contracts. + Fills are indexed independently and uniquely identified by their + chain_type + transaction_hash + log_index combination. + Changes in the schema should be reflected in the bulk import module: - Explorer.Chain.Import.Runner.Signet.Fills @@ -17,21 +20,19 @@ defmodule Explorer.Chain.Signet.Fill do @optional_attrs ~w()a - @required_attrs ~w(outputs_witness_hash chain_type block_number transaction_hash log_index outputs_json)a + @required_attrs ~w(chain_type block_number transaction_hash log_index outputs_json)a @allowed_attrs @optional_attrs ++ @required_attrs @typedoc """ Descriptor of a Signet Filled event: - * `outputs_witness_hash` - keccak256 hash of outputs for correlation with orders - * `chain_type` - Whether this fill occurred on :rollup or :host chain + * `chain_type` - Whether this fill occurred on :rollup or :host chain (primary key) + * `transaction_hash` - The hash of the transaction containing the fill (primary key) + * `log_index` - The index of the log within the transaction (primary key) * `block_number` - The block number where the fill was executed - * `transaction_hash` - The hash of the transaction containing the fill - * `log_index` - The index of the log within the transaction * `outputs_json` - JSON-encoded array of filled outputs """ @type to_import :: %{ - outputs_witness_hash: binary(), chain_type: :rollup | :host, block_number: non_neg_integer(), transaction_hash: binary(), @@ -41,11 +42,10 @@ defmodule Explorer.Chain.Signet.Fill do @primary_key false typed_schema "signet_fills" do - field(:outputs_witness_hash, Hash.Full, primary_key: true) field(:chain_type, Ecto.Enum, values: [:rollup, :host], primary_key: true) + field(:transaction_hash, Hash.Full, primary_key: true) + field(:log_index, :integer, primary_key: true) field(:block_number, :integer) - field(:transaction_hash, Hash.Full) - field(:log_index, :integer) field(:outputs_json, :string) timestamps() @@ -59,7 +59,7 @@ defmodule Explorer.Chain.Signet.Fill do fill |> cast(attrs, @allowed_attrs) |> validate_required(@required_attrs) - |> unique_constraint([:outputs_witness_hash, :chain_type]) + |> unique_constraint([:chain_type, :transaction_hash, :log_index]) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/signet/order.ex b/apps/explorer/lib/explorer/chain/signet/order.ex index 22de10cf4d02..f1c97050d815 100644 --- a/apps/explorer/lib/explorer/chain/signet/order.ex +++ b/apps/explorer/lib/explorer/chain/signet/order.ex @@ -2,6 +2,9 @@ defmodule Explorer.Chain.Signet.Order do @moduledoc """ Models a Signet Order event from the RollupOrders contract. + Orders are indexed independently and uniquely identified by their + transaction_hash + log_index combination. + Changes in the schema should be reflected in the bulk import module: - Explorer.Chain.Import.Runner.Signet.Orders @@ -17,25 +20,23 @@ defmodule Explorer.Chain.Signet.Order do @optional_attrs ~w(sweep_recipient sweep_token sweep_amount)a - @required_attrs ~w(outputs_witness_hash deadline block_number transaction_hash log_index inputs_json outputs_json)a + @required_attrs ~w(deadline block_number transaction_hash log_index inputs_json outputs_json)a @allowed_attrs @optional_attrs ++ @required_attrs @typedoc """ Descriptor of a Signet Order event: - * `outputs_witness_hash` - keccak256 hash of outputs for cross-chain correlation with fills + * `transaction_hash` - The hash of the transaction containing the order (primary key) + * `log_index` - The index of the log within the transaction (primary key) * `deadline` - The deadline timestamp for the order * `block_number` - The block number where the order was created - * `transaction_hash` - The hash of the transaction containing the order - * `log_index` - The index of the log within the transaction * `inputs_json` - JSON-encoded array of input tokens and amounts - * `outputs_json` - JSON-encoded array of output tokens, amounts, and recipients + * `outputs_json` - JSON-encoded array of output tokens, amounts, recipients, and chainIds * `sweep_recipient` - Recipient address from Sweep event (if any) * `sweep_token` - Token address from Sweep event (if any) * `sweep_amount` - Amount from Sweep event (if any) """ @type to_import :: %{ - outputs_witness_hash: binary(), deadline: non_neg_integer(), block_number: non_neg_integer(), transaction_hash: binary(), @@ -49,11 +50,10 @@ defmodule Explorer.Chain.Signet.Order do @primary_key false typed_schema "signet_orders" do - field(:outputs_witness_hash, Hash.Full, primary_key: true) + field(:transaction_hash, Hash.Full, primary_key: true) + field(:log_index, :integer, primary_key: true) field(:deadline, :integer) field(:block_number, :integer) - field(:transaction_hash, Hash.Full) - field(:log_index, :integer) field(:inputs_json, :string) field(:outputs_json, :string) field(:sweep_recipient, Hash.Address) @@ -71,7 +71,7 @@ defmodule Explorer.Chain.Signet.Order do order |> cast(attrs, @allowed_attrs) |> validate_required(@required_attrs) - |> unique_constraint(:outputs_witness_hash) + |> unique_constraint([:transaction_hash, :log_index]) end @doc """ diff --git a/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs b/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs index a4210c738c04..39f8b1bc4741 100644 --- a/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs +++ b/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs @@ -8,12 +8,11 @@ defmodule Explorer.Repo.Signet.Migrations.CreateSignetTables do ) create table(:signet_orders, primary_key: false) do - # Primary key: outputs_witness_hash uniquely identifies an order - add(:outputs_witness_hash, :bytea, null: false, primary_key: true) + # Composite primary key: transaction_hash + log_index uniquely identifies an order + add(:transaction_hash, :bytea, null: false, primary_key: true) + add(:log_index, :integer, null: false, primary_key: true) add(:deadline, :bigint, null: false) add(:block_number, :bigint, null: false) - add(:transaction_hash, :bytea, null: false) - add(:log_index, :integer, null: false) # JSON-encoded input/output arrays for flexibility add(:inputs_json, :text, null: false) add(:outputs_json, :text, null: false) @@ -26,28 +25,20 @@ defmodule Explorer.Repo.Signet.Migrations.CreateSignetTables do # Index for querying orders by block for reorg handling create(index(:signet_orders, [:block_number])) - # Index for querying orders by transaction - create(index(:signet_orders, [:transaction_hash])) # Index for finding unfilled orders by deadline create(index(:signet_orders, [:deadline])) create table(:signet_fills, primary_key: false) do - # Composite primary key: witness_hash + chain_type - # An order can be filled on both rollup and host chains - add(:outputs_witness_hash, :bytea, null: false, primary_key: true) + # Composite primary key: chain_type + transaction_hash + log_index add(:chain_type, :signet_fill_chain_type, null: false, primary_key: true) + add(:transaction_hash, :bytea, null: false, primary_key: true) + add(:log_index, :integer, null: false, primary_key: true) add(:block_number, :bigint, null: false) - add(:transaction_hash, :bytea, null: false) - add(:log_index, :integer, null: false) add(:outputs_json, :text, null: false) timestamps(null: false, type: :utc_datetime_usec) end # Index for querying fills by block for reorg handling create(index(:signet_fills, [:chain_type, :block_number])) - # Index for querying fills by transaction - create(index(:signet_fills, [:transaction_hash])) - # Index for correlating fills back to orders - create(index(:signet_fills, [:outputs_witness_hash])) end end diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index e05a9c205349..933bbb2c176f 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -25,10 +25,12 @@ defmodule Indexer.Fetcher.Signet.EventParser do Sweep(address indexed recipient, address indexed token, uint256 amount) ``` - ## Cross-Chain Correlation + ## Architecture Note - Orders are correlated with fills across chains using the `outputs_witness_hash`, - computed as: keccak256(concat(keccak256(abi.encode(output)) for each output)) + Orders and fills are indexed independently. Direct correlation between orders + and their fills is not possible at the indexer level - only block-level + coordination is available. The data is stored separately for querying and + analytics purposes. """ require Logger @@ -111,40 +113,6 @@ defmodule Indexer.Fetcher.Signet.EventParser do {:ok, fills} end - @doc """ - Compute the outputs_witness_hash for a list of outputs. - - The hash is computed as: keccak256(concat(keccak256(abi_encode(output)) for output in outputs)) - - Output struct (from @signet-sh/sdk): - - token: address - - amount: uint256 - - recipient: address - - chainId: uint32 - """ - @spec compute_outputs_witness_hash([{binary(), non_neg_integer(), binary(), non_neg_integer()}]) :: binary() - def compute_outputs_witness_hash(outputs) do - output_hashes = - outputs - |> Enum.map(fn {token, amount, recipient, chain_id} -> - # ABI-encode each output as (address token, uint256 amount, address recipient, uint32 chainId) - # Padded to 32 bytes each - encoded = - <<0::size(96)>> <> - normalize_address(token) <> - <> <> - <<0::size(96)>> <> - normalize_address(recipient) <> - <<0::size(224)>> <> - <> - - ExKeccak.hash_256(encoded) - end) - |> Enum.join() - - ExKeccak.hash_256(output_hashes) - end - # Parse Order event # Order(uint256 deadline, Input[] inputs, Output[] outputs) defp parse_order_event(log) do @@ -153,10 +121,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do with {:ok, decoded} <- decode_order_data(data) do {deadline, inputs, outputs} = decoded - outputs_witness_hash = compute_outputs_witness_hash(outputs) - order = %{ - outputs_witness_hash: outputs_witness_hash, deadline: deadline, block_number: parse_block_number(log), transaction_hash: get_transaction_hash(log), @@ -175,10 +140,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do data = get_log_data(log) with {:ok, outputs} <- decode_filled_data(data) do - outputs_witness_hash = compute_outputs_witness_hash(outputs) - fill = %{ - outputs_witness_hash: outputs_witness_hash, block_number: parse_block_number(log), transaction_hash: get_transaction_hash(log), log_index: parse_log_index(log), @@ -355,12 +317,6 @@ defmodule Indexer.Fetcher.Signet.EventParser do "0x" <> Base.encode16(bytes, case: :lower) end - defp normalize_address(bytes) when is_binary(bytes) and byte_size(bytes) == 20, do: bytes - - defp normalize_address("0x" <> hex) when byte_size(hex) == 40 do - Base.decode16!(hex, case: :mixed) - end - defp get_topic(log, index) do topics = Map.get(log, "topics") || Map.get(log, :topics) || [] Enum.at(topics, index) diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 550e7094941d..6b9a8d41dd2e 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -2,12 +2,15 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do @moduledoc """ Fetcher for Signet Order and Filled events from RollupOrders and HostOrders contracts. - This module tracks cross-chain orders in the Signet protocol by: + This module indexes Signet protocol events: 1. Parsing Order events from the RollupOrders contract on L2 2. Parsing Filled events from both RollupOrders (L2) and HostOrders (L1) contracts 3. Parsing Sweep events from RollupOrders contract - 4. Computing outputs_witness_hash for cross-chain correlation - 5. Inserting events into signet_orders / signet_fills tables + 4. Inserting events into signet_orders / signet_fills tables + + Note: Orders and fills are indexed independently. Direct correlation between + orders and their fills is not possible at the indexer level - only block-level + coordination is available. ## Event Signatures diff --git a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs index 78d9556989e7..806fcb8b876e 100644 --- a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs @@ -2,11 +2,12 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do @moduledoc """ Unit tests for Indexer.Fetcher.Signet.EventParser module. - Tests verify event parsing, Output struct field ordering, and - outputs_witness_hash computation for cross-chain order correlation. + Tests verify event parsing and Output struct field ordering. Output struct field order (per @signet-sh/sdk): (address token, uint256 amount, address recipient, uint32 chainId) + + Note: Orders and fills are indexed independently - no correlation between them. """ use ExUnit.Case, async: true @@ -19,74 +20,6 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do @test_amount 1_000_000_000_000_000_000 # 1e18 @test_chain_id 1 # Mainnet - describe "compute_outputs_witness_hash/1" do - test "returns 32-byte hash for single output" do - outputs = [{@test_token, @test_amount, @test_recipient, @test_chain_id}] - - hash = EventParser.compute_outputs_witness_hash(outputs) - - assert byte_size(hash) == 32 - end - - test "returns deterministic hash for same outputs" do - outputs = [{@test_token, @test_amount, @test_recipient, @test_chain_id}] - - hash1 = EventParser.compute_outputs_witness_hash(outputs) - hash2 = EventParser.compute_outputs_witness_hash(outputs) - - assert hash1 == hash2 - end - - test "returns different hashes for different outputs" do - outputs1 = [{@test_token, @test_amount, @test_recipient, @test_chain_id}] - outputs2 = [{@test_token, @test_amount + 1, @test_recipient, @test_chain_id}] - - hash1 = EventParser.compute_outputs_witness_hash(outputs1) - hash2 = EventParser.compute_outputs_witness_hash(outputs2) - - refute hash1 == hash2 - end - - test "different chain_id produces different hash" do - outputs1 = [{@test_token, @test_amount, @test_recipient, 1}] - outputs2 = [{@test_token, @test_amount, @test_recipient, 42161}] # Arbitrum - - hash1 = EventParser.compute_outputs_witness_hash(outputs1) - hash2 = EventParser.compute_outputs_witness_hash(outputs2) - - refute hash1 == hash2 - end - - test "order of outputs matters" do - output1 = {@test_token, @test_amount, @test_recipient, @test_chain_id} - output2 = {@test_recipient, @test_amount * 2, @test_token, 42161} - - hash_ordered = EventParser.compute_outputs_witness_hash([output1, output2]) - hash_reversed = EventParser.compute_outputs_witness_hash([output2, output1]) - - refute hash_ordered == hash_reversed - end - - test "handles empty outputs list" do - hash = EventParser.compute_outputs_witness_hash([]) - - # Should still return a valid hash (hash of empty concat) - assert byte_size(hash) == 32 - end - - test "handles multiple outputs" do - outputs = [ - {@test_token, 100, @test_recipient, 1}, - {@test_recipient, 200, @test_token, 42161}, - {@test_token, 300, @test_recipient, 10} # Optimism - ] - - hash = EventParser.compute_outputs_witness_hash(outputs) - - assert byte_size(hash) == 32 - end - end - describe "parse_rollup_logs/1" do test "returns empty lists for empty logs" do {:ok, {orders, fills}} = EventParser.parse_rollup_logs([]) @@ -167,32 +100,6 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do assert extracted_recipient == recipient assert extracted_chain_id == chain_id end - - test "compute_outputs_witness_hash uses correct Output encoding order" do - # When encoding for witness hash, the order must be: - # (token, amount, recipient, chainId) - # NOT the old incorrect order: (recipient, token, amount) - - # Two outputs with same data but different "interpretations" - # would produce different hashes if order is wrong - - token = @test_token - amount = 12345 - recipient = @test_recipient - chain_id = 42161 - - # Correct order: (token, amount, recipient, chainId) - correct_output = [{token, amount, recipient, chain_id}] - - # What the hash would be if we incorrectly swapped token/recipient - incorrect_output = [{recipient, amount, token, chain_id}] - - correct_hash = EventParser.compute_outputs_witness_hash(correct_output) - incorrect_hash = EventParser.compute_outputs_witness_hash(incorrect_output) - - # Hashes must be different - this validates the encoding uses correct order - refute correct_hash == incorrect_hash - end end describe "log field parsing helpers" do @@ -243,4 +150,31 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do assert String.length(sweep_topic) == 66 end end + + describe "parsed event structure" do + test "parsed orders contain expected fields" do + # Orders should have: deadline, block_number, transaction_hash, log_index, inputs_json, outputs_json + # Primary key is (transaction_hash, log_index) + expected_fields = ~w(deadline block_number transaction_hash log_index inputs_json outputs_json)a + + # Verify the expected_fields list is what we're looking for + assert :deadline in expected_fields + assert :block_number in expected_fields + assert :transaction_hash in expected_fields + assert :log_index in expected_fields + assert :inputs_json in expected_fields + assert :outputs_json in expected_fields + end + + test "parsed fills contain expected fields" do + # Fills should have: block_number, transaction_hash, log_index, outputs_json + # Primary key is (chain_type, transaction_hash, log_index) - chain_type added at import + expected_fields = ~w(block_number transaction_hash log_index outputs_json)a + + assert :block_number in expected_fields + assert :transaction_hash in expected_fields + assert :log_index in expected_fields + assert :outputs_json in expected_fields + end + end end From 0cbe64bcba4842be6000236a44f722001c62bd11 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 14:20:57 +0000 Subject: [PATCH 05/24] chore: pin @signet-sh/sdk to version 0.4.4 Per James's request - use exact version 0.4.4 instead of ^0.3.0 --- tools/signet-sdk/package-lock.json | 8 ++++---- tools/signet-sdk/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/signet-sdk/package-lock.json b/tools/signet-sdk/package-lock.json index dcca622d6fb6..9d9925a478cc 100644 --- a/tools/signet-sdk/package-lock.json +++ b/tools/signet-sdk/package-lock.json @@ -8,7 +8,7 @@ "name": "signet-sdk-extractor", "version": "1.0.0", "dependencies": { - "@signet-sh/sdk": "^0.3.0" + "@signet-sh/sdk": "0.4.4" } }, "node_modules/@adraffy/ens-normalize": { @@ -100,9 +100,9 @@ } }, "node_modules/@signet-sh/sdk": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@signet-sh/sdk/-/sdk-0.3.0.tgz", - "integrity": "sha512-mfMvNr5Y+0fDdRuispmwydcCC2UfZg5gkfDUox/nuDQ9OowM/ZHq0zUjQmcBbeGYFBi4aF0uCgi+X10g6y/CWw==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@signet-sh/sdk/-/sdk-0.4.4.tgz", + "integrity": "sha512-+MU8hhzG5I7IbKvern2jWMqi61PIRnJx7jfaAwo6rIM2Pg8EAE1pxPToiVcgeRHtbymSNjMxMrItAYChcmUqrQ==", "license": "MIT OR Apache-2.0", "engines": { "node": ">=20" diff --git a/tools/signet-sdk/package.json b/tools/signet-sdk/package.json index 841b3013fb1a..9e8f317e5db5 100644 --- a/tools/signet-sdk/package.json +++ b/tools/signet-sdk/package.json @@ -8,6 +8,6 @@ "extract": "node extract-abis.mjs" }, "dependencies": { - "@signet-sh/sdk": "^0.3.0" + "@signet-sh/sdk": "0.4.4" } } From 13575dc12ef5a618821f23a8994c2a3c3130b3a1 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 15:04:20 +0000 Subject: [PATCH 06/24] docs: update README to reflect new primary key structure and chainId semantics - Remove outdated outputs_witness_hash correlation documentation - Update database tables to show composite primary keys - Add chainId semantics explanation (destination in orders, origin in fills) - Clarify that orders and fills are indexed independently Per architecture update from James --- .../lib/indexer/fetcher/signet/README.md | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/signet/README.md b/apps/indexer/lib/indexer/fetcher/signet/README.md index f14a675db7fa..719fb51832ad 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/README.md +++ b/apps/indexer/lib/indexer/fetcher/signet/README.md @@ -8,9 +8,13 @@ The Signet protocol enables cross-chain orders between a rollup (L2) and its hos 1. **Parses Order events** from the RollupOrders contract on L2 2. **Parses Filled events** from both RollupOrders (L2) and HostOrders (L1) contracts -3. **Computes outputs_witness_hash** for correlating orders with their fills +3. **Stores events independently** for querying and analytics 4. **Handles chain reorgs** gracefully by removing invalidated data +**Note:** Orders and fills are indexed independently. Direct correlation between orders +and their corresponding fills is not possible at the indexer level — only block-level +coordination is available. This is a protocol-level constraint. + ## Event Types ### RollupOrders Contract (L2) @@ -51,13 +55,12 @@ Stores Order events with their inputs, outputs, and any associated Sweep data. | Column | Type | Description | |--------|------|-------------| -| outputs_witness_hash | bytea | Primary key, keccak256 of outputs | +| transaction_hash | bytea | Primary key (part 1), transaction containing the order | +| log_index | integer | Primary key (part 2), log index within transaction | | deadline | bigint | Order deadline timestamp | | block_number | bigint | Block where order was created | -| transaction_hash | bytea | Transaction containing the order | -| log_index | integer | Log index within transaction | | inputs_json | text | JSON array of inputs | -| outputs_json | text | JSON array of outputs | +| outputs_json | text | JSON array of outputs (includes chainId) | | sweep_recipient | bytea | Sweep recipient (if any) | | sweep_token | bytea | Sweep token (if any) | | sweep_amount | numeric | Sweep amount (if any) | @@ -68,22 +71,20 @@ Stores Filled events from both chains. | Column | Type | Description | |--------|------|-------------| -| outputs_witness_hash | bytea | Part of composite primary key | -| chain_type | enum | 'rollup' or 'host' | +| chain_type | enum | Primary key (part 1), 'rollup' or 'host' | +| transaction_hash | bytea | Primary key (part 2), transaction containing the fill | +| log_index | integer | Primary key (part 3), log index within transaction | | block_number | bigint | Block where fill occurred | -| transaction_hash | bytea | Transaction containing the fill | -| log_index | integer | Log index within transaction | -| outputs_json | text | JSON array of filled outputs | +| outputs_json | text | JSON array of filled outputs (includes chainId) | -## Cross-Chain Correlation +## chainId Semantics -Orders and fills are correlated using `outputs_witness_hash`: +The Output struct includes a `chainId` field with different semantics depending on context: -``` -outputs_witness_hash = keccak256(concat(keccak256(abi_encode(output)) for output in outputs)) -``` +- **In Order events (origin chain):** `chainId` is the **destination chain** where assets should be delivered +- **In Filled events (destination chain):** `chainId` is the **origin chain** where the order was created -This allows matching fills to their original orders even across different chains. +This semantic difference is inherent to the protocol and must be considered when interpreting the data. ## Reorg Handling From 12003bcbda773bfd87be2077e8613697ac12c642 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 16:09:45 +0000 Subject: [PATCH 07/24] test: add signet_order and signet_fill factory entries Phase 2 progress for ENG-1876: Added factory entries for test data generation. - signet_order_factory: generates Order structs with proper JSON fields - signet_fill_factory: generates Fill structs with chain_type and outputs These factories enable integration testing of the signet indexer. --- apps/explorer/test/support/factory.ex | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 378bcc55cd37..3b9f97bf0d19 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -57,6 +57,7 @@ defmodule Explorer.Factory do } alias Explorer.Chain.Optimism.{InteropMessage, OutputRoot} + alias Explorer.Chain.Signet.{Order, Fill} alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Chain.Zilliqa.Hash.BLSPublicKey alias Explorer.Chain.Zilliqa.Staker, as: ZilliqaStaker @@ -1800,4 +1801,44 @@ defmodule Explorer.Factory do meta: nil } end + + def signet_order_factory do + %Order{ + transaction_hash: transaction_hash(), + log_index: Enum.random(0..100), + deadline: DateTime.to_unix(DateTime.utc_now()) + 3600, + block_number: block_number(), + inputs_json: + Jason.encode!([ + %{"token" => "0x" <> String.duplicate("aa", 20), "amount" => "1000000000000000000"} + ]), + outputs_json: + Jason.encode!([ + %{ + "token" => "0x" <> String.duplicate("bb", 20), + "amount" => "1000000000000000000", + "recipient" => "0x" <> String.duplicate("cc", 20), + "chainId" => 1 + } + ]) + } + end + + def signet_fill_factory do + %Fill{ + chain_type: :rollup, + transaction_hash: transaction_hash(), + log_index: Enum.random(0..100), + block_number: block_number(), + outputs_json: + Jason.encode!([ + %{ + "token" => "0x" <> String.duplicate("bb", 20), + "amount" => "1000000000000000000", + "recipient" => "0x" <> String.duplicate("cc", 20), + "chainId" => 1 + } + ]) + } + end end From ced2a25e5761da03e6c01ba7bc1660055fbaaa5b Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 18:36:15 +0000 Subject: [PATCH 08/24] docs: clarify chainId semantics in Order and Fill schema docs Per PR review feedback from @prestwich: - In Order events: chainId = destination chain (where assets should be delivered) - In Filled events: chainId = origin chain (where the order was created) Added inline documentation to the @typedoc in both schema files to make this semantic difference explicit for future developers. --- apps/explorer/lib/explorer/chain/signet/fill.ex | 4 +++- apps/explorer/lib/explorer/chain/signet/order.ex | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex index 1e71eb5a6a23..ccbfcba166d0 100644 --- a/apps/explorer/lib/explorer/chain/signet/fill.ex +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -30,7 +30,9 @@ defmodule Explorer.Chain.Signet.Fill do * `transaction_hash` - The hash of the transaction containing the fill (primary key) * `log_index` - The index of the log within the transaction (primary key) * `block_number` - The block number where the fill was executed - * `outputs_json` - JSON-encoded array of filled outputs + * `outputs_json` - JSON-encoded array of filled outputs (token, amount, recipient, chainId). + NOTE: In Filled events, the `chainId` field represents the ORIGIN chain + (where the order was created), not the chain where the fill occurred. """ @type to_import :: %{ chain_type: :rollup | :host, diff --git a/apps/explorer/lib/explorer/chain/signet/order.ex b/apps/explorer/lib/explorer/chain/signet/order.ex index f1c97050d815..25468ddc7e55 100644 --- a/apps/explorer/lib/explorer/chain/signet/order.ex +++ b/apps/explorer/lib/explorer/chain/signet/order.ex @@ -31,7 +31,9 @@ defmodule Explorer.Chain.Signet.Order do * `deadline` - The deadline timestamp for the order * `block_number` - The block number where the order was created * `inputs_json` - JSON-encoded array of input tokens and amounts - * `outputs_json` - JSON-encoded array of output tokens, amounts, recipients, and chainIds + * `outputs_json` - JSON-encoded array of output tokens, amounts, recipients, and chainIds. + NOTE: In Order events, the `chainId` field represents the DESTINATION chain + (where assets should be delivered), not the chain where the order was created. * `sweep_recipient` - Recipient address from Sweep event (if any) * `sweep_token` - Token address from Sweep event (if any) * `sweep_amount` - Amount from Sweep event (if any) From 9a19e9ec53c48c9baaa1a2568e15c86302534a19 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Tue, 17 Feb 2026 22:09:03 +0000 Subject: [PATCH 09/24] test: Add integration tests for Signet Orders Indexer Phase 2 - Integration Tests: ## New Test Files Explorer Tests: - apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs - Tests for Order import runner (insert, upsert, batch operations) - Tests composite primary key (transaction_hash, log_index) - Tests sweep data handling - apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs - Tests for Fill import runner (rollup/host fills) - Tests composite primary key (chain_type, transaction_hash, log_index) - Tests same transaction on different chains Indexer Tests: - apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs - Full pipeline tests via Chain.import - ReorgHandler tests (rollup and host reorgs) - Db utility function tests - Factory integration tests ## Bug Fix - apps/indexer/lib/indexer/fetcher/signet/utils/db.ex - Updated Db utility functions to use correct primary key fields - Replaced outputs_witness_hash references with transaction_hash - Added get_order_by_tx_and_log, get_orders_for_transaction - Added get_fill, get_fills_for_transaction Closes ENG-1876 Phase 2 --- .../chain/import/runner/signet/fills_test.exs | 217 ++++++++++++ .../import/runner/signet/orders_test.exs | 175 ++++++++++ .../lib/indexer/fetcher/signet/utils/db.ex | 53 ++- .../fetcher/signet/orders_fetcher_test.exs | 330 ++++++++++++++++++ 4 files changed, 748 insertions(+), 27 deletions(-) create mode 100644 apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs create mode 100644 apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs new file mode 100644 index 000000000000..e8c3746fc66f --- /dev/null +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -0,0 +1,217 @@ +defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do + use Explorer.DataCase + + alias Ecto.Multi + alias Explorer.Chain.Import.Runner.Signet.Fills, as: FillsRunner + alias Explorer.Chain.Signet.Fill + alias Explorer.Repo + + @moduletag :signet + + describe "run/3" do + test "inserts a new rollup fill" do + tx_hash = <<1::256>> + + params = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = + Multi.new() + |> FillsRunner.run(params, %{timestamps: timestamps}) + + assert {:ok, %{insert_signet_fills: [fill]}} = Repo.transaction(multi) + assert fill.block_number == 100 + assert fill.chain_type == :rollup + assert fill.log_index == 0 + end + + test "inserts a new host fill" do + tx_hash = <<2::256>> + + params = [ + %{ + chain_type: :host, + block_number: 200, + transaction_hash: tx_hash, + log_index: 1, + outputs_json: Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_fills: [fill]}} = Repo.transaction(multi) + + assert fill.block_number == 200 + assert fill.chain_type == :host + end + + test "same transaction can have fills on different chains" do + tx_hash = <<3::256>> + + rollup_params = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + } + ] + + host_params = [ + %{ + chain_type: :host, + block_number: 200, + transaction_hash: tx_hash, + log_index: 0, # Same log_index but different chain_type + outputs_json: Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi1 = Multi.new() |> FillsRunner.run(rollup_params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi1) + + multi2 = Multi.new() |> FillsRunner.run(host_params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi2) + + # Should have two fills (one per chain type) + assert Repo.aggregate(Fill, :count) == 2 + + tx_hash_struct = %Explorer.Chain.Hash.Full{byte_count: 32, bytes: tx_hash} + + # Verify both exist + rollup_fill = Repo.get_by(Fill, + chain_type: :rollup, + transaction_hash: tx_hash_struct, + log_index: 0 + ) + host_fill = Repo.get_by(Fill, + chain_type: :host, + transaction_hash: tx_hash_struct, + log_index: 0 + ) + + assert rollup_fill.block_number == 100 + assert host_fill.block_number == 200 + end + + test "handles duplicate fills with upsert on composite primary key" do + tx_hash = <<4::256>> + + params1 = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}]) + } + ] + + params2 = [ + %{ + chain_type: :rollup, + block_number: 101, # Different block + transaction_hash: tx_hash, + log_index: 0, # Same log_index + chain_type + outputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi1 = Multi.new() |> FillsRunner.run(params1, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi1) + + multi2 = Multi.new() |> FillsRunner.run(params2, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi2) + + # Should only have one fill for this chain_type + tx_hash + log_index combo + assert Repo.aggregate(Fill, :count) == 1 + + fill = Repo.one!(Fill) + assert fill.block_number == 101 # Updated + end + + test "different log_index creates separate fills" do + tx_hash = <<5::256>> + + params = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}]) + }, + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 1, # Different log_index + outputs_json: Jason.encode!([%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_fills: fills}} = Repo.transaction(multi) + + assert length(fills) == 2 + assert Repo.aggregate(Fill, :count) == 2 + end + + test "inserts multiple fills in batch" do + params = + for i <- 1..5 do + %{ + chain_type: if(rem(i, 2) == 0, do: :host, else: :rollup), + block_number: 100 + i, + transaction_hash: <<100 + i::256>>, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"}]) + } + end + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_fills: fills}} = Repo.transaction(multi) + + assert length(fills) == 5 + assert Repo.aggregate(Fill, :count) == 5 + + # Verify chain type distribution + rollup_count = Enum.count(fills, &(&1.chain_type == :rollup)) + host_count = Enum.count(fills, &(&1.chain_type == :host)) + assert rollup_count == 3 # i = 1, 3, 5 + assert host_count == 2 # i = 2, 4 + end + end + + describe "ecto_schema_module/0" do + test "returns Fill module" do + assert FillsRunner.ecto_schema_module() == Fill + end + end + + describe "option_key/0" do + test "returns :signet_fills" do + assert FillsRunner.option_key() == :signet_fills + end + end +end diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs new file mode 100644 index 000000000000..a7ac4dc21212 --- /dev/null +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -0,0 +1,175 @@ +defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do + use Explorer.DataCase + + alias Ecto.Multi + alias Explorer.Chain.Import.Runner.Signet.Orders, as: OrdersRunner + alias Explorer.Chain.Signet.Order + alias Explorer.Repo + + @moduletag :signet + + describe "run/3" do + test "inserts a new order" do + tx_hash = <<1::256>> + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = + Multi.new() + |> OrdersRunner.run(params, %{timestamps: timestamps}) + + assert {:ok, %{insert_signet_orders: [order]}} = Repo.transaction(multi) + assert order.block_number == 100 + assert order.deadline == 1_700_000_000 + assert order.log_index == 0 + end + + test "handles duplicate orders with upsert on composite primary key" do + tx_hash = <<2::256>> + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + # Insert first time + multi1 = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi1) + + # Insert second time with same tx_hash + log_index but different data + updated_params = [ + %{ + deadline: 1_700_000_001, # Different deadline + block_number: 101, # Different block + transaction_hash: tx_hash, + log_index: 0, # Same log_index + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "2000"}]), + outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}]) + } + ] + + multi2 = Multi.new() |> OrdersRunner.run(updated_params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi2) + + # Should only have one order + assert Repo.aggregate(Order, :count) == 1 + + # Order should be updated + tx_hash_struct = %Explorer.Chain.Hash.Full{byte_count: 32, bytes: tx_hash} + order = Repo.get_by(Order, transaction_hash: tx_hash_struct, log_index: 0) + assert order.deadline == 1_700_000_001 + assert order.block_number == 101 + end + + test "different log_index creates separate orders" do + tx_hash = <<3::256>> + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + }, + %{ + deadline: 1_700_000_001, + block_number: 100, + transaction_hash: tx_hash, + log_index: 1, # Different log_index + inputs_json: Jason.encode!([%{"token" => "0x4444", "amount" => "2000"}]), + outputs_json: Jason.encode!([%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_orders: orders}} = Repo.transaction(multi) + + assert length(orders) == 2 + assert Repo.aggregate(Order, :count) == 2 + end + + test "inserts order with sweep data" do + tx_hash = <<4::256>> + sweep_recipient = <<5::160>> + sweep_token = <<6::160>> + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), + sweep_recipient: sweep_recipient, + sweep_token: sweep_token, + sweep_amount: Decimal.new("12345") + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_orders: [order]}} = Repo.transaction(multi) + + assert order.sweep_amount == %Explorer.Chain.Wei{value: Decimal.new("12345")} + end + + test "inserts multiple orders in batch" do + params = + for i <- 1..5 do + %{ + deadline: 1_700_000_000 + i, + block_number: 100 + i, + transaction_hash: <<100 + i::256>>, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}]), + outputs_json: Jason.encode!([%{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"}]) + } + end + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_orders: orders}} = Repo.transaction(multi) + + assert length(orders) == 5 + assert Repo.aggregate(Order, :count) == 5 + end + end + + describe "ecto_schema_module/0" do + test "returns Order module" do + assert OrdersRunner.ecto_schema_module() == Order + end + end + + describe "option_key/0" do + test "returns :signet_orders" do + assert OrdersRunner.option_key() == :signet_orders + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex index faaa0ff86c86..78f7ec62e54e 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex @@ -38,54 +38,53 @@ defmodule Indexer.Fetcher.Signet.Utils.Db do end @doc """ - Get an order by its outputs_witness_hash. + Get an order by its transaction hash and log index. """ - @spec get_order_by_witness_hash(binary()) :: Order.t() | nil - def get_order_by_witness_hash(witness_hash) do + @spec get_order_by_tx_and_log(binary(), non_neg_integer()) :: Order.t() | nil + def get_order_by_tx_and_log(transaction_hash, log_index) do Repo.one( from(o in Order, - where: o.outputs_witness_hash == ^witness_hash + where: o.transaction_hash == ^transaction_hash and o.log_index == ^log_index ) ) end @doc """ - Get all fills for a specific order by witness hash. + Get all orders for a specific transaction. """ - @spec get_fills_for_order(binary()) :: [Fill.t()] - def get_fills_for_order(witness_hash) do + @spec get_orders_for_transaction(binary()) :: [Order.t()] + def get_orders_for_transaction(transaction_hash) do Repo.all( - from(f in Fill, - where: f.outputs_witness_hash == ^witness_hash, - order_by: [asc: f.chain_type, asc: f.block_number] + from(o in Order, + where: o.transaction_hash == ^transaction_hash, + order_by: [asc: o.log_index] ) ) end @doc """ - Check if an order has been filled on a specific chain. + Get a fill by its composite primary key. """ - @spec order_filled_on_chain?(binary(), :rollup | :host) :: boolean() - def order_filled_on_chain?(witness_hash, chain_type) do - Repo.exists?( + @spec get_fill(atom(), binary(), non_neg_integer()) :: Fill.t() | nil + def get_fill(chain_type, transaction_hash, log_index) do + Repo.one( from(f in Fill, - where: f.outputs_witness_hash == ^witness_hash and f.chain_type == ^chain_type + where: f.chain_type == ^chain_type and + f.transaction_hash == ^transaction_hash and + f.log_index == ^log_index ) ) end @doc """ - Get unfilled orders (orders without any corresponding fills). + Get all fills for a specific transaction. """ - @spec get_unfilled_orders(non_neg_integer()) :: [Order.t()] - def get_unfilled_orders(limit \\ 100) do + @spec get_fills_for_transaction(binary()) :: [Fill.t()] + def get_fills_for_transaction(transaction_hash) do Repo.all( - from(o in Order, - left_join: f in Fill, - on: o.outputs_witness_hash == f.outputs_witness_hash, - where: is_nil(f.outputs_witness_hash), - limit: ^limit, - order_by: [desc: o.block_number] + from(f in Fill, + where: f.transaction_hash == ^transaction_hash, + order_by: [asc: f.chain_type, asc: f.log_index] ) ) end @@ -108,13 +107,13 @@ defmodule Indexer.Fetcher.Signet.Utils.Db do """ @spec get_order_fill_counts() :: %{orders: non_neg_integer(), rollup_fills: non_neg_integer(), host_fills: non_neg_integer()} def get_order_fill_counts do - orders_count = Repo.one(from(o in Order, select: count(o.outputs_witness_hash))) + orders_count = Repo.one(from(o in Order, select: count(o.transaction_hash))) rollup_fills_count = - Repo.one(from(f in Fill, where: f.chain_type == :rollup, select: count(f.outputs_witness_hash))) + Repo.one(from(f in Fill, where: f.chain_type == :rollup, select: count(f.transaction_hash))) host_fills_count = - Repo.one(from(f in Fill, where: f.chain_type == :host, select: count(f.outputs_witness_hash))) + Repo.one(from(f in Fill, where: f.chain_type == :host, select: count(f.transaction_hash))) %{ orders: orders_count || 0, diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs new file mode 100644 index 000000000000..b1512af9ec61 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -0,0 +1,330 @@ +defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do + @moduledoc """ + Integration tests for the Signet OrdersFetcher module. + + These tests verify the full pipeline from event fetching through + database insertion, including reorg handling and database utilities. + + Note: Orders and fills are indexed independently with no correlation. + Primary keys are: + - Orders: (transaction_hash, log_index) + - Fills: (chain_type, transaction_hash, log_index) + """ + + use Explorer.DataCase, async: false + + import Explorer.Factory + + alias Explorer.Chain + alias Explorer.Chain.Signet.{Order, Fill} + alias Explorer.Repo + alias Indexer.Fetcher.Signet.{OrdersFetcher, ReorgHandler} + alias Indexer.Fetcher.Signet.Utils.Db + + @moduletag :signet + + describe "OrdersFetcher configuration" do + test "child_spec returns proper supervisor config" do + json_rpc_named_arguments = [ + transport: EthereumJSONRPC.Mox, + transport_options: [] + ] + + Application.put_env(:indexer, OrdersFetcher, + enabled: true, + rollup_orders_address: "0x1234567890123456789012345678901234567890", + recheck_interval: 1000 + ) + + child_spec = OrdersFetcher.child_spec([ + [json_rpc_named_arguments: json_rpc_named_arguments], + [name: OrdersFetcher] + ]) + + assert child_spec.id == OrdersFetcher + assert child_spec.restart == :transient + end + end + + describe "database import via Chain.import/1" do + test "imports order through Chain.import" do + tx_hash = <<1::256>> + + order_params = %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + + assert {:ok, %{insert_signet_orders: [order]}} = + Chain.import(%{ + signet_orders: %{params: [order_params]}, + timeout: :infinity + }) + + assert order.block_number == 100 + assert order.deadline == 1_700_000_000 + end + + test "imports fill through Chain.import" do + tx_hash = <<2::256>> + + fill_params = %{ + chain_type: :rollup, + block_number: 150, + transaction_hash: tx_hash, + log_index: 1, + outputs_json: Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + } + + assert {:ok, %{insert_signet_fills: [fill]}} = + Chain.import(%{ + signet_fills: %{params: [fill_params]}, + timeout: :infinity + }) + + assert fill.block_number == 150 + assert fill.chain_type == :rollup + end + + test "imports order and fill together" do + order_params = %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: <<10::256>>, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + } + + fill_params = %{ + chain_type: :host, + block_number: 200, + transaction_hash: <<20::256>>, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + } + + assert {:ok, result} = + Chain.import(%{ + signet_orders: %{params: [order_params]}, + signet_fills: %{params: [fill_params]}, + timeout: :infinity + }) + + assert length(result.insert_signet_orders) == 1 + assert length(result.insert_signet_fills) == 1 + end + end + + describe "ReorgHandler" do + test "rollup reorg removes orders and rollup fills from affected blocks" do + # Insert test data + insert_test_order(<<1::256>>, 100) + insert_test_order(<<2::256>>, 200) + insert_test_order(<<3::256>>, 300) + + insert_test_fill(:rollup, <<11::256>>, 150) + insert_test_fill(:rollup, <<12::256>>, 250) + insert_test_fill(:host, <<13::256>>, 250) # Host fill should survive rollup reorg + + assert Repo.aggregate(Order, :count) == 3 + assert Repo.aggregate(Fill, :count) == 3 + + # Trigger reorg from block 200 + ReorgHandler.handle_reorg(200, :rollup) + + # Orders from block 200+ should be deleted + assert Repo.aggregate(Order, :count) == 1 + remaining_order = Repo.one(Order) + assert remaining_order.block_number == 100 + + # Rollup fills from block 200+ should be deleted + fills = Repo.all(Fill) + assert length(fills) == 2 + rollup_fills = Enum.filter(fills, &(&1.chain_type == :rollup)) + host_fills = Enum.filter(fills, &(&1.chain_type == :host)) + assert length(rollup_fills) == 1 + assert hd(rollup_fills).block_number == 150 + # Host fill should remain + assert length(host_fills) == 1 + end + + test "host reorg only removes host fills from affected blocks" do + # Insert test data + insert_test_order(<<1::256>>, 100) + insert_test_fill(:rollup, <<11::256>>, 150) + insert_test_fill(:host, <<21::256>>, 200) + insert_test_fill(:host, <<22::256>>, 300) + + # Trigger host reorg from block 250 + ReorgHandler.handle_reorg(250, :host) + + # Order should remain + assert Repo.aggregate(Order, :count) == 1 + + # Rollup fill should remain + fills = Repo.all(Fill) + rollup_fills = Enum.filter(fills, &(&1.chain_type == :rollup)) + host_fills = Enum.filter(fills, &(&1.chain_type == :host)) + + assert length(rollup_fills) == 1 + assert length(host_fills) == 1 # Only host fill at block 200 remains + assert hd(host_fills).block_number == 200 + end + + test "reorg at genesis deletes all data" do + insert_test_order(<<1::256>>, 100) + insert_test_order(<<2::256>>, 200) + insert_test_fill(:rollup, <<11::256>>, 150) + insert_test_fill(:host, <<21::256>>, 250) + + ReorgHandler.handle_reorg(0, :rollup) + + assert Repo.aggregate(Order, :count) == 0 + rollup_fills = Repo.all(from(f in Fill, where: f.chain_type == :rollup)) + assert length(rollup_fills) == 0 + # Host fill should remain even in rollup reorg + host_fills = Repo.all(from(f in Fill, where: f.chain_type == :host)) + assert length(host_fills) == 1 + end + end + + describe "Db utility functions" do + test "highest_indexed_order_block returns correct value" do + assert Db.highest_indexed_order_block(0) == 0 + + insert_test_order(<<1::256>>, 100) + insert_test_order(<<2::256>>, 200) + insert_test_order(<<3::256>>, 150) + + assert Db.highest_indexed_order_block(0) == 200 + end + + test "highest_indexed_fill_block returns correct value per chain" do + assert Db.highest_indexed_fill_block(:rollup, 0) == 0 + assert Db.highest_indexed_fill_block(:host, 0) == 0 + + insert_test_fill(:rollup, <<11::256>>, 100) + insert_test_fill(:rollup, <<12::256>>, 200) + insert_test_fill(:host, <<21::256>>, 150) + insert_test_fill(:host, <<22::256>>, 300) + + assert Db.highest_indexed_fill_block(:rollup, 0) == 200 + assert Db.highest_indexed_fill_block(:host, 0) == 300 + end + + test "get_orders_by_deadline_range returns orders in range" do + insert_test_order_with_deadline(<<1::256>>, 100, 1_000) + insert_test_order_with_deadline(<<2::256>>, 200, 2_000) + insert_test_order_with_deadline(<<3::256>>, 300, 3_000) + + orders = Db.get_orders_by_deadline_range(1_500, 2_500) + assert length(orders) == 1 + assert hd(orders).deadline == 2_000 + end + + test "get_order_fill_counts returns accurate counts" do + insert_test_order(<<1::256>>, 100) + insert_test_order(<<2::256>>, 200) + insert_test_fill(:rollup, <<11::256>>, 150) + insert_test_fill(:rollup, <<12::256>>, 250) + insert_test_fill(:host, <<21::256>>, 300) + + counts = Db.get_order_fill_counts() + + assert counts.orders == 2 + assert counts.rollup_fills == 2 + assert counts.host_fills == 1 + end + end + + describe "factory integration" do + test "signet_order factory creates valid order" do + order = insert(:signet_order) + + assert order.transaction_hash != nil + assert order.log_index != nil + assert order.deadline != nil + assert order.block_number != nil + assert order.inputs_json != nil + assert order.outputs_json != nil + end + + test "signet_fill factory creates valid fill" do + fill = insert(:signet_fill) + + assert fill.transaction_hash != nil + assert fill.log_index != nil + assert fill.chain_type in [:rollup, :host] + assert fill.block_number != nil + assert fill.outputs_json != nil + end + + test "factory orders can be customized" do + order = insert(:signet_order, deadline: 9999999999, block_number: 42) + + assert order.deadline == 9999999999 + assert order.block_number == 42 + end + + test "factory fills can be customized" do + fill = insert(:signet_fill, chain_type: :host, block_number: 123) + + assert fill.chain_type == :host + assert fill.block_number == 123 + end + end + + # Helper functions for test data insertion + + defp insert_test_order(tx_hash, block_number) do + params = %{ + deadline: 1_700_000_000, + block_number: block_number, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0xabc", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0xdef", "recipient" => "0x123", "amount" => "500", "chainId" => "1"}]) + } + + {:ok, %{insert_signet_orders: [order]}} = + Chain.import(%{signet_orders: %{params: [params]}, timeout: :infinity}) + + order + end + + defp insert_test_order_with_deadline(tx_hash, block_number, deadline) do + params = %{ + deadline: deadline, + block_number: block_number, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0xabc", "amount" => "1000"}]), + outputs_json: Jason.encode!([%{"token" => "0xdef", "recipient" => "0x123", "amount" => "500", "chainId" => "1"}]) + } + + {:ok, %{insert_signet_orders: [order]}} = + Chain.import(%{signet_orders: %{params: [params]}, timeout: :infinity}) + + order + end + + defp insert_test_fill(chain_type, tx_hash, block_number) do + params = %{ + chain_type: chain_type, + block_number: block_number, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: Jason.encode!([%{"token" => "0xfff", "recipient" => "0x999", "amount" => "500", "chainId" => "1"}]) + } + + {:ok, %{insert_signet_fills: [fill]}} = + Chain.import(%{signet_fills: %{params: [params]}, timeout: :infinity}) + + fill + end +end From d12385d4e1eb0b1793b81b334668d887df5077ef Mon Sep 17 00:00:00 2001 From: init4samwise Date: Wed, 18 Feb 2026 00:05:20 +0000 Subject: [PATCH 10/24] fix: Address code review feedback for Signet Orders Indexer - Add missing 'import Ecto.Query' in orders_fetcher.ex (compilation fix) - Add bounded l2_rpc_block_range for rollup eth_getLogs calls - Wrap reorg cleanup operations in transaction for atomicity - Add runtime.exs configuration for Signet OrdersFetcher - Update module documentation with all config options Addresses code review feedback from James on ENG-1876 --- .../indexer/fetcher/signet/orders_fetcher.ex | 17 +++++++--- .../indexer/fetcher/signet/reorg_handler.ex | 33 ++++++++++--------- config/runtime.exs | 15 +++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 6b9a8d41dd2e..f4473838ccdb 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -32,7 +32,10 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do host_orders_address: "0x...", l1_rpc: "https://...", l1_rpc_block_range: 1000, - recheck_interval: 15_000 + l2_rpc_block_range: 1000, + recheck_interval: 15_000, + start_block: 0, + failure_interval_threshold: 600_000 ## Architecture @@ -46,6 +49,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do require Logger + import Ecto.Query + alias Explorer.Chain alias Explorer.Chain.Signet.{Order, Fill} alias Indexer.BufferedTask @@ -80,6 +85,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do host_orders_address = config[:host_orders_address] l1_rpc = config[:l1_rpc] l1_rpc_block_range = config[:l1_rpc_block_range] || 1000 + l2_rpc_block_range = config[:l2_rpc_block_range] || 1000 recheck_interval = config[:recheck_interval] || 15_000 start_block = config[:start_block] || 0 @@ -97,6 +103,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do rollup_orders_address: rollup_orders_address, host_orders_address: host_orders_address, l1_rpc_block_range: l1_rpc_block_range, + l2_rpc_block_range: l2_rpc_block_range, recheck_interval: recheck_interval, failure_interval_threshold: failure_interval_threshold, start_block: start_block @@ -281,21 +288,23 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do start_block = state.task_data.check_new_rollup.start_block with {:ok, latest_block} <- get_latest_block(config.json_l2_rpc_named_arguments), + # Bound the block range to avoid unbounded eth_getLogs calls + end_block = min(start_block + config.l2_rpc_block_range, latest_block), {:ok, logs} <- fetch_logs( config.json_l2_rpc_named_arguments, config.rollup_orders_address, start_block, - latest_block + end_block ), {:ok, {orders, fills}} <- EventParser.parse_rollup_logs(logs), :ok <- import_orders(orders), :ok <- import_fills(fills, :rollup) do Logger.info( - "Processed rollup events: #{length(orders)} orders, #{length(fills)} fills (blocks #{start_block}-#{latest_block})" + "Processed rollup events: #{length(orders)} orders, #{length(fills)} fills (blocks #{start_block}-#{end_block})" ) - updated_task_data = put_in(state.task_data.check_new_rollup.start_block, latest_block + 1) + updated_task_data = put_in(state.task_data.check_new_rollup.start_block, end_block + 1) {:ok, %{state | task_data: updated_task_data}} end end diff --git a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex index 1090786c7a71..bef37154cf82 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex @@ -46,25 +46,28 @@ defmodule Indexer.Fetcher.Signet.ReorgHandler do # Handle reorg on the rollup (L2) chain # This affects both orders and rollup fills defp handle_rollup_reorg(from_block) do - # Delete orders at or after the reorg block - {deleted_orders, _} = - Repo.delete_all( - from(o in Order, - where: o.block_number >= ^from_block + # Wrap in transaction to ensure atomicity + Repo.transaction(fn -> + # Delete orders at or after the reorg block + {deleted_orders, _} = + Repo.delete_all( + from(o in Order, + where: o.block_number >= ^from_block + ) ) - ) - # Delete rollup fills at or after the reorg block - {deleted_fills, _} = - Repo.delete_all( - from(f in Fill, - where: f.chain_type == :rollup and f.block_number >= ^from_block + # Delete rollup fills at or after the reorg block + {deleted_fills, _} = + Repo.delete_all( + from(f in Fill, + where: f.chain_type == :rollup and f.block_number >= ^from_block + ) ) - ) - Logger.info( - "Rollup reorg cleanup: deleted #{deleted_orders} orders, #{deleted_fills} fills from block #{from_block}" - ) + Logger.info( + "Rollup reorg cleanup: deleted #{deleted_orders} orders, #{deleted_fills} fills from block #{from_block}" + ) + end) end # Handle reorg on the host (L1) chain diff --git a/config/runtime.exs b/config/runtime.exs index 92f4a3df9268..62b878e9ffd1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1482,6 +1482,21 @@ config :indexer, Indexer.Fetcher.Arbitrum.DataBackfill.Supervisor, ConfigHelper.chain_type() != :arbitrum || not ConfigHelper.parse_bool_env_var("INDEXER_ARBITRUM_DATA_BACKFILL_ENABLED") +# Signet Orders Indexer configuration +config :indexer, Indexer.Fetcher.Signet.OrdersFetcher, + enabled: ConfigHelper.parse_bool_env_var("INDEXER_SIGNET_ORDERS_ENABLED", "false"), + rollup_orders_address: System.get_env("INDEXER_SIGNET_ROLLUP_ORDERS_ADDRESS"), + host_orders_address: System.get_env("INDEXER_SIGNET_HOST_ORDERS_ADDRESS"), + l1_rpc: System.get_env("INDEXER_SIGNET_L1_RPC"), + l1_rpc_block_range: ConfigHelper.parse_integer_env_var("INDEXER_SIGNET_L1_RPC_BLOCK_RANGE", 1_000), + l2_rpc_block_range: ConfigHelper.parse_integer_env_var("INDEXER_SIGNET_L2_RPC_BLOCK_RANGE", 1_000), + recheck_interval: ConfigHelper.parse_time_env_var("INDEXER_SIGNET_RECHECK_INTERVAL", "15s"), + start_block: ConfigHelper.parse_integer_env_var("INDEXER_SIGNET_START_BLOCK", 0), + failure_interval_threshold: ConfigHelper.parse_time_env_var("INDEXER_SIGNET_FAILURE_THRESHOLD", "10m") + +config :indexer, Indexer.Fetcher.Signet.OrdersFetcher.Supervisor, + enabled: ConfigHelper.parse_bool_env_var("INDEXER_SIGNET_ORDERS_ENABLED", "false") + config :indexer, Indexer.Fetcher.RootstockData.Supervisor, disabled?: ConfigHelper.chain_type() != :rsk || ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_ROOTSTOCK_DATA_FETCHER") From fb4d722c9fb9f200363cb8c16502be0c2dbc5d25 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Feb 2026 09:16:13 -0500 Subject: [PATCH 11/24] fix: Resolve CI failures for Signet Orders Indexer - Fix compilation error in factory.ex where Kernel.+/2 was unavailable in ExMachina context - Remove duplicate supervisor.ex that conflicted with auto-generated one from `use Indexer.Fetcher` - Fix Credo issues: reduce complexity in parse_rollup_logs by extracting helpers, replace explicit try with implicit try, sort alias groups - Run mix format on all signet files Co-Authored-By: Claude Opus 4.6 --- .../chain/import/runner/signet/fills.ex | 2 +- .../chain/import/runner/signet/orders.ex | 2 +- .../chain/import/runner/signet/fills_test.exs | 73 +++++--- .../import/runner/signet/orders_test.exs | 35 ++-- apps/explorer/test/support/factory.ex | 2 +- .../indexer/fetcher/signet/event_parser.ex | 165 +++++++++--------- .../indexer/fetcher/signet/orders_fetcher.ex | 6 +- .../signet/orders_fetcher/supervisor.ex | 36 ---- .../indexer/fetcher/signet/reorg_handler.ex | 2 +- .../lib/indexer/fetcher/signet/utils/db.ex | 15 +- .../test/indexer/fetcher/signet/abi_test.exs | 38 ++-- .../fetcher/signet/event_parser_test.exs | 49 +++--- .../fetcher/signet/orders_fetcher_test.exs | 31 ++-- 13 files changed, 230 insertions(+), 226 deletions(-) delete mode 100644 apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex diff --git a/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex b/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex index 958a8be0ef67..88eb0486edb6 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/signet/fills.ex @@ -8,8 +8,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.Fills do import Ecto.Query, only: [from: 2] alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Signet.Fill alias Explorer.Chain.Import + alias Explorer.Chain.Signet.Fill alias Explorer.Prometheus.Instrumenter @behaviour Import.Runner diff --git a/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex b/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex index b182137418c9..8dcde8bbd3d8 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/signet/orders.ex @@ -8,8 +8,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.Orders do import Ecto.Query, only: [from: 2] alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Signet.Order alias Explorer.Chain.Import + alias Explorer.Chain.Signet.Order alias Explorer.Prometheus.Instrumenter @behaviour Import.Runner diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs index e8c3746fc66f..079e30111dd1 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -18,7 +18,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) } ] @@ -43,7 +44,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do block_number: 200, transaction_hash: tx_hash, log_index: 1, - outputs_json: Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) } ] @@ -65,7 +67,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) } ] @@ -74,8 +77,10 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do chain_type: :host, block_number: 200, transaction_hash: tx_hash, - log_index: 0, # Same log_index but different chain_type - outputs_json: Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + # Same log_index but different chain_type + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) } ] @@ -93,16 +98,19 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do tx_hash_struct = %Explorer.Chain.Hash.Full{byte_count: 32, bytes: tx_hash} # Verify both exist - rollup_fill = Repo.get_by(Fill, - chain_type: :rollup, - transaction_hash: tx_hash_struct, - log_index: 0 - ) - host_fill = Repo.get_by(Fill, - chain_type: :host, - transaction_hash: tx_hash_struct, - log_index: 0 - ) + rollup_fill = + Repo.get_by(Fill, + chain_type: :rollup, + transaction_hash: tx_hash_struct, + log_index: 0 + ) + + host_fill = + Repo.get_by(Fill, + chain_type: :host, + transaction_hash: tx_hash_struct, + log_index: 0 + ) assert rollup_fill.block_number == 100 assert host_fill.block_number == 200 @@ -117,17 +125,21 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}]) } ] params2 = [ %{ chain_type: :rollup, - block_number: 101, # Different block + # Different block + block_number: 101, transaction_hash: tx_hash, - log_index: 0, # Same log_index + chain_type - outputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}]) + # Same log_index + chain_type + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}]) } ] @@ -143,7 +155,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do assert Repo.aggregate(Fill, :count) == 1 fill = Repo.one!(Fill) - assert fill.block_number == 101 # Updated + # Updated + assert fill.block_number == 101 end test "different log_index creates separate fills" do @@ -155,14 +168,17 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}]) }, %{ chain_type: :rollup, block_number: 100, transaction_hash: tx_hash, - log_index: 1, # Different log_index - outputs_json: Jason.encode!([%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}]) + # Different log_index + log_index: 1, + outputs_json: + Jason.encode!([%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}]) } ] @@ -183,7 +199,10 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do block_number: 100 + i, transaction_hash: <<100 + i::256>>, log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"}]) + outputs_json: + Jason.encode!([ + %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} + ]) } end @@ -198,8 +217,10 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do # Verify chain type distribution rollup_count = Enum.count(fills, &(&1.chain_type == :rollup)) host_count = Enum.count(fills, &(&1.chain_type == :host)) - assert rollup_count == 3 # i = 1, 3, 5 - assert host_count == 2 # i = 2, 4 + # i = 1, 3, 5 + assert rollup_count == 3 + # i = 2, 4 + assert host_count == 2 end end diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs index a7ac4dc21212..bbfd772607d4 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -19,7 +19,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do transaction_hash: tx_hash, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) } ] @@ -45,7 +46,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do transaction_hash: tx_hash, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) } ] @@ -58,12 +60,16 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do # Insert second time with same tx_hash + log_index but different data updated_params = [ %{ - deadline: 1_700_000_001, # Different deadline - block_number: 101, # Different block + # Different deadline + deadline: 1_700_000_001, + # Different block + block_number: 101, transaction_hash: tx_hash, - log_index: 0, # Same log_index + # Same log_index + log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "2000"}]), - outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}]) } ] @@ -90,15 +96,18 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do transaction_hash: tx_hash, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) }, %{ deadline: 1_700_000_001, block_number: 100, transaction_hash: tx_hash, - log_index: 1, # Different log_index + # Different log_index + log_index: 1, inputs_json: Jason.encode!([%{"token" => "0x4444", "amount" => "2000"}]), - outputs_json: Jason.encode!([%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}]) } ] @@ -123,7 +132,8 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do transaction_hash: tx_hash, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), sweep_recipient: sweep_recipient, sweep_token: sweep_token, sweep_amount: Decimal.new("12345") @@ -147,7 +157,10 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do transaction_hash: <<100 + i::256>>, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}]), - outputs_json: Jason.encode!([%{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"}]) + outputs_json: + Jason.encode!([ + %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} + ]) } end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 3b9f97bf0d19..f6096dc95183 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1806,7 +1806,7 @@ defmodule Explorer.Factory do %Order{ transaction_hash: transaction_hash(), log_index: Enum.random(0..100), - deadline: DateTime.to_unix(DateTime.utc_now()) + 3600, + deadline: DateTime.utc_now() |> DateTime.to_unix() |> Kernel.+(3600), block_number: block_number(), inputs_json: Jason.encode!([ diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index 933bbb2c176f..ba60e39f58c0 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -45,42 +45,9 @@ defmodule Indexer.Fetcher.Signet.EventParser do """ @spec parse_rollup_logs([map()]) :: {:ok, {[map()], [map()]}} def parse_rollup_logs(logs) when is_list(logs) do - order_topic = Abi.order_event_topic() - filled_topic = Abi.filled_event_topic() - sweep_topic = Abi.sweep_event_topic() - {orders, fills, sweeps} = - Enum.reduce(logs, {[], [], []}, fn log, {orders_acc, fills_acc, sweeps_acc} -> - topic = get_topic(log, 0) - - cond do - topic == order_topic -> - case parse_order_event(log) do - {:ok, order} -> {[order | orders_acc], fills_acc, sweeps_acc} - {:error, reason} -> - Logger.warning("Failed to parse Order event: #{inspect(reason)}") - {orders_acc, fills_acc, sweeps_acc} - end - - topic == filled_topic -> - case parse_filled_event(log) do - {:ok, fill} -> {orders_acc, [fill | fills_acc], sweeps_acc} - {:error, reason} -> - Logger.warning("Failed to parse Filled event: #{inspect(reason)}") - {orders_acc, fills_acc, sweeps_acc} - end - - topic == sweep_topic -> - case parse_sweep_event(log) do - {:ok, sweep} -> {orders_acc, fills_acc, [sweep | sweeps_acc]} - {:error, reason} -> - Logger.warning("Failed to parse Sweep event: #{inspect(reason)}") - {orders_acc, fills_acc, sweeps_acc} - end - - true -> - {orders_acc, fills_acc, sweeps_acc} - end + Enum.reduce(logs, {[], [], []}, fn log, acc -> + classify_and_parse_log(log, acc) end) # Associate sweeps with their corresponding orders by transaction hash @@ -89,6 +56,38 @@ defmodule Indexer.Fetcher.Signet.EventParser do {:ok, {Enum.reverse(orders_with_sweeps), Enum.reverse(fills)}} end + defp classify_and_parse_log(log, {orders_acc, fills_acc, sweeps_acc}) do + topic = get_topic(log, 0) + + cond do + topic == Abi.order_event_topic() -> + collect_parsed(parse_order_event(log), "Order", orders_acc, fills_acc, sweeps_acc, :order) + + topic == Abi.filled_event_topic() -> + collect_parsed(parse_filled_event(log), "Filled", orders_acc, fills_acc, sweeps_acc, :fill) + + topic == Abi.sweep_event_topic() -> + collect_parsed(parse_sweep_event(log), "Sweep", orders_acc, fills_acc, sweeps_acc, :sweep) + + true -> + {orders_acc, fills_acc, sweeps_acc} + end + end + + defp collect_parsed({:ok, item}, _label, orders, fills, sweeps, :order), + do: {[item | orders], fills, sweeps} + + defp collect_parsed({:ok, item}, _label, orders, fills, sweeps, :fill), + do: {orders, [item | fills], sweeps} + + defp collect_parsed({:ok, item}, _label, orders, fills, sweeps, :sweep), + do: {orders, fills, [item | sweeps]} + + defp collect_parsed({:error, reason}, label, orders, fills, sweeps, _slot) do + Logger.warning("Failed to parse #{label} event: #{inspect(reason)}") + {orders, fills, sweeps} + end + @doc """ Parse Filled events from the HostOrders contract. @@ -103,7 +102,9 @@ defmodule Indexer.Fetcher.Signet.EventParser do |> Enum.filter(fn log -> get_topic(log, 0) == filled_topic end) |> Enum.map(&parse_filled_event/1) |> Enum.filter(fn - {:ok, _} -> true + {:ok, _} -> + true + {:error, reason} -> Logger.warning("Failed to parse host Filled event: #{inspect(reason)}") false @@ -178,27 +179,23 @@ defmodule Indexer.Fetcher.Signet.EventParser do # Input = (address token, uint256 amount) # Output = (address token, uint256 amount, address recipient, uint32 chainId) defp decode_order_data(data) when is_binary(data) do - try do - # ABI decode: uint256, dynamic array offset, dynamic array offset - <> = data - - # Parse inputs array - offset is from start of data (after first 32 bytes) - inputs_data = binary_part(rest, inputs_offset - 96, byte_size(rest) - inputs_offset + 96) - inputs = decode_input_array(inputs_data) - - # Parse outputs array - outputs_data = binary_part(rest, outputs_offset - 96, byte_size(rest) - outputs_offset + 96) - outputs = decode_output_array(outputs_data) - - {:ok, {deadline, inputs, outputs}} - rescue - e -> - Logger.error("Error decoding Order data: #{inspect(e)}") - {:error, :decode_failed} - end + # ABI decode: uint256, dynamic array offset, dynamic array offset + <> = data + + # Parse inputs array - offset is from start of data (after first 32 bytes) + inputs_data = binary_part(rest, inputs_offset - 96, byte_size(rest) - inputs_offset + 96) + inputs = decode_input_array(inputs_data) + + # Parse outputs array + outputs_data = binary_part(rest, outputs_offset - 96, byte_size(rest) - outputs_offset + 96) + outputs = decode_output_array(outputs_data) + + {:ok, {deadline, inputs, outputs}} + rescue + e -> + Logger.error("Error decoding Order data: #{inspect(e)}") + {:error, :decode_failed} end defp decode_order_data(_), do: {:error, :invalid_data} @@ -206,15 +203,13 @@ defmodule Indexer.Fetcher.Signet.EventParser do # Decode Filled event data # Filled(Output[] outputs) defp decode_filled_data(data) when is_binary(data) do - try do - <<_offset::unsigned-big-integer-size(256), rest::binary>> = data - outputs = decode_output_array(rest) - {:ok, outputs} - rescue - e -> - Logger.error("Error decoding Filled data: #{inspect(e)}") - {:error, :decode_failed} - end + <<_offset::unsigned-big-integer-size(256), rest::binary>> = data + outputs = decode_output_array(rest) + {:ok, outputs} + rescue + e -> + Logger.error("Error decoding Filled data: #{inspect(e)}") + {:error, :decode_failed} end defp decode_filled_data(_), do: {:error, :invalid_data} @@ -222,14 +217,12 @@ defmodule Indexer.Fetcher.Signet.EventParser do # Decode Sweep event data # Only amount is in data (recipient and token are indexed) defp decode_sweep_data(data) when is_binary(data) do - try do - <> = data - {:ok, amount} - rescue - e -> - Logger.error("Error decoding Sweep data: #{inspect(e)}") - {:error, :decode_failed} - end + <> = data + {:ok, amount} + rescue + e -> + Logger.error("Error decoding Sweep data: #{inspect(e)}") + {:error, :decode_failed} end defp decode_sweep_data(_), do: {:error, :invalid_data} @@ -242,10 +235,11 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_inputs(_data, 0, acc), do: Enum.reverse(acc) - defp decode_inputs(<<_padding::binary-size(12), - token::binary-size(20), - amount::unsigned-big-integer-size(256), - rest::binary>>, count, acc) do + defp decode_inputs( + <<_padding::binary-size(12), token::binary-size(20), amount::unsigned-big-integer-size(256), rest::binary>>, + count, + acc + ) do input = {token, amount} decode_inputs(rest, count - 1, [input | acc]) end @@ -258,14 +252,13 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_outputs(_data, 0, acc), do: Enum.reverse(acc) - defp decode_outputs(<<_padding1::binary-size(12), - token::binary-size(20), - amount::unsigned-big-integer-size(256), - _padding2::binary-size(12), - recipient::binary-size(20), - _padding3::binary-size(28), - chain_id::unsigned-big-integer-size(32), - rest::binary>>, count, acc) do + defp decode_outputs( + <<_padding1::binary-size(12), token::binary-size(20), amount::unsigned-big-integer-size(256), + _padding2::binary-size(12), recipient::binary-size(20), _padding3::binary-size(28), + chain_id::unsigned-big-integer-size(32), rest::binary>>, + count, + acc + ) do # Output struct order: token, amount, recipient, chainId output = {token, amount, recipient, chain_id} decode_outputs(rest, count - 1, [output | acc]) diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index f4473838ccdb..bc26f25af642 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -52,7 +52,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do import Ecto.Query alias Explorer.Chain - alias Explorer.Chain.Signet.{Order, Fill} + alias Explorer.Chain.Signet.{Fill, Order} alias Indexer.BufferedTask alias Indexer.Fetcher.Signet.{Abi, EventParser, ReorgHandler} alias Indexer.Helper, as: IndexerHelper @@ -325,9 +325,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do ), {:ok, fills} <- EventParser.parse_host_filled_logs(logs), :ok <- import_fills(fills, :host) do - Logger.info( - "Processed host events: #{length(fills)} fills (blocks #{start_block}-#{end_block})" - ) + Logger.info("Processed host events: #{length(fills)} fills (blocks #{start_block}-#{end_block})") updated_task_data = put_in(state.task_data.check_new_host.start_block, end_block + 1) {:ok, %{state | task_data: updated_task_data}} diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex deleted file mode 100644 index 93b314462148..000000000000 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher/supervisor.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Indexer.Fetcher.Signet.OrdersFetcher.Supervisor do - @moduledoc """ - Supervises the Signet OrdersFetcher and its task supervisor. - """ - - use Supervisor - - alias Indexer.Fetcher.Signet.OrdersFetcher - - def child_spec(init_arg) do - %{ - id: __MODULE__, - start: {__MODULE__, :start_link, [init_arg]}, - type: :supervisor - } - end - - def start_link(init_arg) do - Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) - end - - def disabled? do - config = Application.get_env(:indexer, OrdersFetcher, []) - not Keyword.get(config, :enabled, false) - end - - @impl Supervisor - def init(init_arg) do - children = [ - {Task.Supervisor, name: OrdersFetcher.TaskSupervisor}, - {OrdersFetcher, init_arg} - ] - - Supervisor.init(children, strategy: :one_for_one) - end -end diff --git a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex index bef37154cf82..9bbc42c07de1 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex @@ -11,7 +11,7 @@ defmodule Indexer.Fetcher.Signet.ReorgHandler do import Ecto.Query - alias Explorer.Chain.Signet.{Order, Fill} + alias Explorer.Chain.Signet.{Fill, Order} alias Explorer.Repo @doc """ diff --git a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex index 78f7ec62e54e..ee3c38efad75 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex @@ -5,7 +5,7 @@ defmodule Indexer.Fetcher.Signet.Utils.Db do import Ecto.Query - alias Explorer.Chain.Signet.{Order, Fill} + alias Explorer.Chain.Signet.{Fill, Order} alias Explorer.Repo @doc """ @@ -69,9 +69,10 @@ defmodule Indexer.Fetcher.Signet.Utils.Db do def get_fill(chain_type, transaction_hash, log_index) do Repo.one( from(f in Fill, - where: f.chain_type == ^chain_type and - f.transaction_hash == ^transaction_hash and - f.log_index == ^log_index + where: + f.chain_type == ^chain_type and + f.transaction_hash == ^transaction_hash and + f.log_index == ^log_index ) ) end @@ -105,7 +106,11 @@ defmodule Indexer.Fetcher.Signet.Utils.Db do @doc """ Count orders and fills for metrics. """ - @spec get_order_fill_counts() :: %{orders: non_neg_integer(), rollup_fills: non_neg_integer(), host_fills: non_neg_integer()} + @spec get_order_fill_counts() :: %{ + orders: non_neg_integer(), + rollup_fills: non_neg_integer(), + host_fills: non_neg_integer() + } def get_order_fill_counts do orders_count = Repo.one(from(o in Order, select: count(o.transaction_hash))) diff --git a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs index c87a46220c9a..c738289e49db 100644 --- a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs @@ -1,7 +1,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do @moduledoc """ Unit tests for Indexer.Fetcher.Signet.Abi module. - + Tests verify event topic hash computation and ABI loading functionality. """ @@ -12,11 +12,11 @@ defmodule Indexer.Fetcher.Signet.AbiTest do describe "event topic hashes" do test "order_event_topic/0 returns valid keccak256 hash" do topic = Abi.order_event_topic() - + # Should be a hex string starting with 0x and 64 hex chars (32 bytes) assert String.starts_with?(topic, "0x") assert String.length(topic) == 66 - + # Verify it's a valid hex string "0x" <> hex = topic assert {:ok, _} = Base.decode16(hex, case: :lower) @@ -24,20 +24,20 @@ defmodule Indexer.Fetcher.Signet.AbiTest do test "filled_event_topic/0 returns valid keccak256 hash" do topic = Abi.filled_event_topic() - + assert String.starts_with?(topic, "0x") assert String.length(topic) == 66 - + "0x" <> hex = topic assert {:ok, _} = Base.decode16(hex, case: :lower) end test "sweep_event_topic/0 returns valid keccak256 hash" do topic = Abi.sweep_event_topic() - + assert String.starts_with?(topic, "0x") assert String.length(topic) == 66 - + "0x" <> hex = topic assert {:ok, _} = Base.decode16(hex, case: :lower) end @@ -46,7 +46,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do order_topic = Abi.order_event_topic() filled_topic = Abi.filled_event_topic() sweep_topic = Abi.sweep_event_topic() - + refute order_topic == filled_topic refute order_topic == sweep_topic refute filled_topic == sweep_topic @@ -63,7 +63,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do describe "event signatures" do test "order_event_signature/0 returns expected format" do sig = Abi.order_event_signature() - + # Should follow format: Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) assert String.starts_with?(sig, "Order(") assert String.contains?(sig, "uint256") @@ -72,7 +72,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do test "filled_event_signature/0 returns expected format" do sig = Abi.filled_event_signature() - + # Should follow format: Filled((address,uint256,address,uint32)[]) assert String.starts_with?(sig, "Filled(") assert String.contains?(sig, "uint32") @@ -80,7 +80,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do test "sweep_event_signature/0 returns expected format" do sig = Abi.sweep_event_signature() - + # Should follow format: Sweep(address,address,uint256) assert String.starts_with?(sig, "Sweep(") assert String.contains?(sig, "address") @@ -90,14 +90,14 @@ defmodule Indexer.Fetcher.Signet.AbiTest do describe "rollup_orders_event_topics/0" do test "returns list of three topics" do topics = Abi.rollup_orders_event_topics() - + assert is_list(topics) assert length(topics) == 3 end test "includes all rollup event topics" do topics = Abi.rollup_orders_event_topics() - + assert Abi.order_event_topic() in topics assert Abi.filled_event_topic() in topics assert Abi.sweep_event_topic() in topics @@ -107,7 +107,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do describe "host_orders_event_topics/0" do test "returns list with only filled topic" do topics = Abi.host_orders_event_topics() - + assert is_list(topics) assert length(topics) == 1 assert Abi.filled_event_topic() in topics @@ -117,13 +117,13 @@ defmodule Indexer.Fetcher.Signet.AbiTest do describe "abi_path/1" do test "returns path for rollup_orders contract" do path = Abi.abi_path("rollup_orders") - + assert String.contains?(path, "priv/contracts_abi/signet/rollup_orders.json") end test "returns path for host_orders contract" do path = Abi.abi_path("host_orders") - + assert String.contains?(path, "priv/contracts_abi/signet/host_orders.json") end end @@ -131,7 +131,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do describe "load_abi/1" do test "loads rollup_orders ABI successfully" do result = Abi.load_abi("rollup_orders") - + assert {:ok, abi} = result assert is_list(abi) assert length(abi) > 0 @@ -139,7 +139,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do test "loads host_orders ABI successfully" do result = Abi.load_abi("host_orders") - + assert {:ok, abi} = result assert is_list(abi) assert length(abi) > 0 @@ -147,7 +147,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do test "returns error for nonexistent contract" do result = Abi.load_abi("nonexistent_contract") - + assert {:error, :not_found} = result end end diff --git a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs index 806fcb8b876e..3248e8546dab 100644 --- a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs @@ -1,12 +1,12 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do @moduledoc """ Unit tests for Indexer.Fetcher.Signet.EventParser module. - + Tests verify event parsing and Output struct field ordering. - + Output struct field order (per @signet-sh/sdk): (address token, uint256 amount, address recipient, uint32 chainId) - + Note: Orders and fills are indexed independently - no correlation between them. """ @@ -17,13 +17,15 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do # Test addresses (20 bytes each) @test_token <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>> @test_recipient <<21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40>> - @test_amount 1_000_000_000_000_000_000 # 1e18 - @test_chain_id 1 # Mainnet + # 1e18 + @test_amount 1_000_000_000_000_000_000 + # Mainnet + @test_chain_id 1 describe "parse_rollup_logs/1" do test "returns empty lists for empty logs" do {:ok, {orders, fills}} = EventParser.parse_rollup_logs([]) - + assert orders == [] assert fills == [] end @@ -38,9 +40,9 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do "logIndex" => "0x0" } ] - + {:ok, {orders, fills}} = EventParser.parse_rollup_logs(logs) - + assert orders == [] assert fills == [] end @@ -49,7 +51,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do describe "parse_host_filled_logs/1" do test "returns empty list for empty logs" do {:ok, fills} = EventParser.parse_host_filled_logs([]) - + assert fills == [] end @@ -63,9 +65,9 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do "logIndex" => "0x0" } ] - + {:ok, fills} = EventParser.parse_host_filled_logs(logs) - + assert fills == [] end end @@ -83,18 +85,18 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do # # This is the correct field order used in the EventParser tuple: # {token, amount, recipient, chain_id} - + token = @test_token amount = @test_amount recipient = @test_recipient chain_id = @test_chain_id - + # The tuple format used in EventParser output_tuple = {token, amount, recipient, chain_id} - + # Extract fields in the expected SDK order {extracted_token, extracted_amount, extracted_recipient, extracted_chain_id} = output_tuple - + assert extracted_token == token assert extracted_amount == amount assert extracted_recipient == recipient @@ -106,11 +108,12 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do test "handles hex-encoded block numbers" do # Test that block_number parsing works for hex strings log = %{ - "blockNumber" => "0x10", # 16 in decimal + # 16 in decimal + "blockNumber" => "0x10", "topics" => [], "data" => "0x" } - + # The parser should correctly decode hex block numbers # This is implicitly tested through parse_rollup_logs/1 {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) @@ -122,7 +125,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do :topics => [], :data => "" } - + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) end end @@ -130,7 +133,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do describe "event topic matching" do test "Order event topic matches Abi module" do order_topic = Abi.order_event_topic() - + # Verify the topic format assert String.starts_with?(order_topic, "0x") assert String.length(order_topic) == 66 @@ -138,14 +141,14 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do test "Filled event topic matches Abi module" do filled_topic = Abi.filled_event_topic() - + assert String.starts_with?(filled_topic, "0x") assert String.length(filled_topic) == 66 end test "Sweep event topic matches Abi module" do sweep_topic = Abi.sweep_event_topic() - + assert String.starts_with?(sweep_topic, "0x") assert String.length(sweep_topic) == 66 end @@ -156,7 +159,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do # Orders should have: deadline, block_number, transaction_hash, log_index, inputs_json, outputs_json # Primary key is (transaction_hash, log_index) expected_fields = ~w(deadline block_number transaction_hash log_index inputs_json outputs_json)a - + # Verify the expected_fields list is what we're looking for assert :deadline in expected_fields assert :block_number in expected_fields @@ -170,7 +173,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do # Fills should have: block_number, transaction_hash, log_index, outputs_json # Primary key is (chain_type, transaction_hash, log_index) - chain_type added at import expected_fields = ~w(block_number transaction_hash log_index outputs_json)a - + assert :block_number in expected_fields assert :transaction_hash in expected_fields assert :log_index in expected_fields diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs index b1512af9ec61..5158cdd6cfbe 100644 --- a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -36,10 +36,11 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do recheck_interval: 1000 ) - child_spec = OrdersFetcher.child_spec([ - [json_rpc_named_arguments: json_rpc_named_arguments], - [name: OrdersFetcher] - ]) + child_spec = + OrdersFetcher.child_spec([ + [json_rpc_named_arguments: json_rpc_named_arguments], + [name: OrdersFetcher] + ]) assert child_spec.id == OrdersFetcher assert child_spec.restart == :transient @@ -56,7 +57,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do transaction_hash: tx_hash, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) } assert {:ok, %{insert_signet_orders: [order]}} = @@ -77,7 +79,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do block_number: 150, transaction_hash: tx_hash, log_index: 1, - outputs_json: Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) } assert {:ok, %{insert_signet_fills: [fill]}} = @@ -97,7 +100,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do transaction_hash: <<10::256>>, log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) } fill_params = %{ @@ -105,7 +109,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do block_number: 200, transaction_hash: <<20::256>>, log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + outputs_json: + Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) } assert {:ok, result} = @@ -129,7 +134,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do insert_test_fill(:rollup, <<11::256>>, 150) insert_test_fill(:rollup, <<12::256>>, 250) - insert_test_fill(:host, <<13::256>>, 250) # Host fill should survive rollup reorg + # Host fill should survive rollup reorg + insert_test_fill(:host, <<13::256>>, 250) assert Repo.aggregate(Order, :count) == 3 assert Repo.aggregate(Fill, :count) == 3 @@ -172,7 +178,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do host_fills = Enum.filter(fills, &(&1.chain_type == :host)) assert length(rollup_fills) == 1 - assert length(host_fills) == 1 # Only host fill at block 200 remains + # Only host fill at block 200 remains + assert length(host_fills) == 1 assert hd(host_fills).block_number == 200 end @@ -265,9 +272,9 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do end test "factory orders can be customized" do - order = insert(:signet_order, deadline: 9999999999, block_number: 42) + order = insert(:signet_order, deadline: 9_999_999_999, block_number: 42) - assert order.deadline == 9999999999 + assert order.deadline == 9_999_999_999 assert order.block_number == 42 end From c422d01628656e8d0d69452da2e43b9fc69587b2 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Feb 2026 09:25:11 -0500 Subject: [PATCH 12/24] refactor: remove dead code from Signet Orders Indexer Remove unused modules, files, and tests that were never called at runtime: - reorg_handler.ex (no callers in polling loop) - utils/db.ex (duplicated fetcher queries, only used in tests) - 8 unused ABI JSON files (only rollup_orders and host_orders are referenced) - tools/signet-sdk/ extract tooling (developer utility, not runtime) - Abi.load_abi/1 and abi_path/1 (never called at runtime) - OrdersFetcher.handle_reorg/2 (delegated to deleted module) - event_parser_test.exs (tautological tests verifying nothing) - README.md from lib/ directory Also fixes Abi topic hashes to be actual compile-time module attributes instead of recomputed on every function call. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 - .../contracts_abi/signet/bundle_helper.json | 184 ------- .../contracts_abi/signet/events_index.json | 462 ---------------- .../priv/contracts_abi/signet/passage.json | 504 ------------------ .../priv/contracts_abi/signet/permit2.json | 23 - .../contracts_abi/signet/rollup_passage.json | 280 ---------- .../priv/contracts_abi/signet/transactor.json | 294 ---------- .../priv/contracts_abi/signet/weth.json | 60 --- .../priv/contracts_abi/signet/zenith.json | 291 ---------- .../lib/indexer/fetcher/signet/README.md | 119 ----- .../indexer/lib/indexer/fetcher/signet/abi.ex | 123 +---- .../indexer/fetcher/signet/orders_fetcher.ex | 12 +- .../indexer/fetcher/signet/reorg_handler.ex | 113 ---- .../lib/indexer/fetcher/signet/utils/db.ex | 129 ----- .../test/indexer/fetcher/signet/abi_test.exs | 116 +--- .../fetcher/signet/event_parser_test.exs | 183 ------- .../fetcher/signet/orders_fetcher_test.exs | 182 +------ tools/signet-sdk/README.md | 74 --- tools/signet-sdk/extract-abis.mjs | 67 --- tools/signet-sdk/package-lock.json | 244 --------- tools/signet-sdk/package.json | 13 - 21 files changed, 28 insertions(+), 3448 deletions(-) delete mode 100644 apps/explorer/priv/contracts_abi/signet/bundle_helper.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/events_index.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/passage.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/permit2.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/rollup_passage.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/transactor.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/weth.json delete mode 100644 apps/explorer/priv/contracts_abi/signet/zenith.json delete mode 100644 apps/indexer/lib/indexer/fetcher/signet/README.md delete mode 100644 apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/signet/utils/db.ex delete mode 100644 apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs delete mode 100644 tools/signet-sdk/README.md delete mode 100644 tools/signet-sdk/extract-abis.mjs delete mode 100644 tools/signet-sdk/package-lock.json delete mode 100644 tools/signet-sdk/package.json diff --git a/.gitignore b/.gitignore index 5357ad0ed7c5..f3545add61b3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,6 @@ npm-debug.log # Static artifacts /apps/**/node_modules -# Tools dependencies -/tools/**/node_modules - # Since we are building assets from assets/, # we ignore priv/static. You may want to comment # this depending on your deployment strategy. diff --git a/apps/explorer/priv/contracts_abi/signet/bundle_helper.json b/apps/explorer/priv/contracts_abi/signet/bundle_helper.json deleted file mode 100644 index 7301cb81ed94..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/bundle_helper.json +++ /dev/null @@ -1,184 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_zenith", - "type": "address", - "internalType": "address" - }, - { - "name": "_orders", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "orders", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract HostOrders" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "submit", - "inputs": [ - { - "name": "fills", - "type": "tuple[]", - "internalType": "struct BundleHelper.FillPermit2[]", - "components": [ - { - "name": "outputs", - "type": "tuple[]", - "internalType": "struct IOrders.Output[]", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "recipient", - "type": "address", - "internalType": "address" - }, - { - "name": "chainId", - "type": "uint32", - "internalType": "uint32" - } - ] - }, - { - "name": "permit2", - "type": "tuple", - "internalType": "struct UsesPermit2.Permit2Batch", - "components": [ - { - "name": "permit", - "type": "tuple", - "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", - "components": [ - { - "name": "permitted", - "type": "tuple[]", - "internalType": "struct ISignatureTransfer.TokenPermissions[]", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "nonce", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "deadline", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "owner", - "type": "address", - "internalType": "address" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ] - }, - { - "name": "header", - "type": "tuple", - "internalType": "struct Zenith.BlockHeader", - "components": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "hostBlockNumber", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gasLimit", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rewardAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "blockDataHash", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "v", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "r", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "s", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "zenith", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract Zenith" - } - ], - "stateMutability": "view" - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/events_index.json b/apps/explorer/priv/contracts_abi/signet/events_index.json deleted file mode 100644 index 3cba039af03a..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/events_index.json +++ /dev/null @@ -1,462 +0,0 @@ -[ - { - "contract": "rollup_orders", - "name": "Filled", - "signature": "Filled(tuple[])", - "inputs": [ - { - "name": "outputs", - "type": "tuple[]", - "indexed": false, - "internalType": "struct IOrders.Output[]", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "recipient", - "type": "address", - "internalType": "address" - }, - { - "name": "chainId", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ] - }, - { - "contract": "rollup_orders", - "name": "Order", - "signature": "Order(uint256,tuple[],tuple[])", - "inputs": [ - { - "name": "deadline", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "inputs", - "type": "tuple[]", - "indexed": false, - "internalType": "struct IOrders.Input[]", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "outputs", - "type": "tuple[]", - "indexed": false, - "internalType": "struct IOrders.Output[]", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "recipient", - "type": "address", - "internalType": "address" - }, - { - "name": "chainId", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ] - }, - { - "contract": "rollup_orders", - "name": "Sweep", - "signature": "Sweep(address,address,uint256)", - "inputs": [ - { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "host_orders", - "name": "Filled", - "signature": "Filled(tuple[])", - "inputs": [ - { - "name": "outputs", - "type": "tuple[]", - "indexed": false, - "internalType": "struct IOrders.Output[]", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "recipient", - "type": "address", - "internalType": "address" - }, - { - "name": "chainId", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ] - }, - { - "contract": "passage", - "name": "Enter", - "signature": "Enter(uint256,address,uint256)", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "passage", - "name": "EnterConfigured", - "signature": "EnterConfigured(address,bool)", - "inputs": [ - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "canEnter", - "type": "bool", - "indexed": true, - "internalType": "bool" - } - ] - }, - { - "contract": "passage", - "name": "EnterToken", - "signature": "EnterToken(uint256,address,address,uint256)", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "passage", - "name": "Withdrawal", - "signature": "Withdrawal(address,address,uint256)", - "inputs": [ - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "rollup_passage", - "name": "Exit", - "signature": "Exit(address,uint256)", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "rollup_passage", - "name": "ExitToken", - "signature": "ExitToken(address,address,uint256)", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "weth", - "name": "Deposit", - "signature": "Deposit(address,uint256)", - "inputs": [ - { - "name": "dst", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "wad", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "weth", - "name": "Withdrawal", - "signature": "Withdrawal(address,uint256)", - "inputs": [ - { - "name": "src", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "wad", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "zenith", - "name": "BlockSubmitted", - "signature": "BlockSubmitted(address,uint256,uint256,address,bytes32)", - "inputs": [ - { - "name": "sequencer", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "gasLimit", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "rewardAddress", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "blockDataHash", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - } - ] - }, - { - "contract": "zenith", - "name": "SequencerSet", - "signature": "SequencerSet(address,bool)", - "inputs": [ - { - "name": "sequencer", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "permissioned", - "type": "bool", - "indexed": true, - "internalType": "bool" - } - ] - }, - { - "contract": "transactor", - "name": "GasConfigured", - "signature": "GasConfigured(uint256,uint256)", - "inputs": [ - { - "name": "perBlock", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "perTransact", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - }, - { - "contract": "transactor", - "name": "Transact", - "signature": "Transact(uint256,address,address,bytes,uint256,uint256,uint256)", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "sender", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "data", - "type": "bytes", - "indexed": false, - "internalType": "bytes" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "gas", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "maxFeePerGas", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ] - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/passage.json b/apps/explorer/priv/contracts_abi/signet/passage.json deleted file mode 100644 index 7fec8c89792a..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/passage.json +++ /dev/null @@ -1,504 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_defaultRollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "_tokenAdmin", - "type": "address", - "internalType": "address" - }, - { - "name": "initialEnterTokens", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "_permit2", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "fallback", - "stateMutability": "payable" - }, - { - "type": "receive", - "stateMutability": "payable" - }, - { - "type": "function", - "name": "canEnter", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "configureEnter", - "inputs": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "_canEnter", - "type": "bool", - "internalType": "bool" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "defaultRollupChainId", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "enter", - "inputs": [ - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "enter", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "enterToken", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "enterToken", - "inputs": [ - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "enterTokenPermit2", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - }, - { - "name": "permit2", - "type": "tuple", - "internalType": "struct UsesPermit2.Permit2", - "components": [ - { - "name": "permit", - "type": "tuple", - "internalType": "struct ISignatureTransfer.PermitTransferFrom", - "components": [ - { - "name": "permitted", - "type": "tuple", - "internalType": "struct ISignatureTransfer.TokenPermissions", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "nonce", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "deadline", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "owner", - "type": "address", - "internalType": "address" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "enterWitness", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "_witness", - "type": "tuple", - "internalType": "struct UsesPermit2.Witness", - "components": [ - { - "name": "witnessHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "witnessTypeString", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "exitWitness", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "_witness", - "type": "tuple", - "internalType": "struct UsesPermit2.Witness", - "components": [ - { - "name": "witnessHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "witnessTypeString", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "tokenAdmin", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "withdraw", - "inputs": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "recipient", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "Enter", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "EnterConfigured", - "inputs": [ - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "canEnter", - "type": "bool", - "indexed": true, - "internalType": "bool" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "EnterToken", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Withdrawal", - "inputs": [ - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "AddressEmptyCode", - "inputs": [ - { - "name": "target", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "DisallowedEnter", - "inputs": [ - { - "name": "token", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "FailedCall", - "inputs": [] - }, - { - "type": "error", - "name": "InsufficientBalance", - "inputs": [ - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "OnlyTokenAdmin", - "inputs": [] - }, - { - "type": "error", - "name": "ReentrancyGuardReentrantCall", - "inputs": [] - }, - { - "type": "error", - "name": "SafeERC20FailedOperation", - "inputs": [ - { - "name": "token", - "type": "address", - "internalType": "address" - } - ] - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/permit2.json b/apps/explorer/priv/contracts_abi/signet/permit2.json deleted file mode 100644 index b00239edbace..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/permit2.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "inputs": [ - { - "name": "owner", - "type": "address" - }, - { - "name": "wordPosition", - "type": "uint256" - } - ], - "name": "nonceBitmap", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/rollup_passage.json b/apps/explorer/priv/contracts_abi/signet/rollup_passage.json deleted file mode 100644 index 905279436053..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/rollup_passage.json +++ /dev/null @@ -1,280 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_permit2", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "fallback", - "stateMutability": "payable" - }, - { - "type": "receive", - "stateMutability": "payable" - }, - { - "type": "function", - "name": "enterWitness", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rollupRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "_witness", - "type": "tuple", - "internalType": "struct UsesPermit2.Witness", - "components": [ - { - "name": "witnessHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "witnessTypeString", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "exit", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "exitToken", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "exitTokenPermit2", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "internalType": "address" - }, - { - "name": "permit2", - "type": "tuple", - "internalType": "struct UsesPermit2.Permit2", - "components": [ - { - "name": "permit", - "type": "tuple", - "internalType": "struct ISignatureTransfer.PermitTransferFrom", - "components": [ - { - "name": "permitted", - "type": "tuple", - "internalType": "struct ISignatureTransfer.TokenPermissions", - "components": [ - { - "name": "token", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "nonce", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "deadline", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "owner", - "type": "address", - "internalType": "address" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "exitWitness", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "_witness", - "type": "tuple", - "internalType": "struct UsesPermit2.Witness", - "components": [ - { - "name": "witnessHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "witnessTypeString", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "pure" - }, - { - "type": "event", - "name": "Exit", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ExitToken", - "inputs": [ - { - "name": "hostRecipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "token", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "AddressEmptyCode", - "inputs": [ - { - "name": "target", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "FailedCall", - "inputs": [] - }, - { - "type": "error", - "name": "InsufficientBalance", - "inputs": [ - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ReentrancyGuardReentrantCall", - "inputs": [] - }, - { - "type": "error", - "name": "SafeERC20FailedOperation", - "inputs": [ - { - "name": "token", - "type": "address", - "internalType": "address" - } - ] - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/transactor.json b/apps/explorer/priv/contracts_abi/signet/transactor.json deleted file mode 100644 index 7b6bfa8b1609..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/transactor.json +++ /dev/null @@ -1,294 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_defaultRollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "_gasAdmin", - "type": "address", - "internalType": "address" - }, - { - "name": "_passage", - "type": "address", - "internalType": "contract Passage" - }, - { - "name": "_perBlockGasLimit", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "_perTransactGasLimit", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "configureGas", - "inputs": [ - { - "name": "perBlock", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "perTransact", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "defaultRollupChainId", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "gasAdmin", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "passage", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract Passage" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "perBlockGasLimit", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "perTransactGasLimit", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "transact", - "inputs": [ - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gas", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "maxFeePerGas", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "transact", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gas", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "maxFeePerGas", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "transactGasUsed", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "event", - "name": "GasConfigured", - "inputs": [ - { - "name": "perBlock", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "perTransact", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Transact", - "inputs": [ - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "sender", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "data", - "type": "bytes", - "indexed": false, - "internalType": "bytes" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "gas", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "maxFeePerGas", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "OnlyGasAdmin", - "inputs": [] - }, - { - "type": "error", - "name": "PerBlockTransactGasLimit", - "inputs": [] - }, - { - "type": "error", - "name": "PerTransactGasLimit", - "inputs": [] - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/weth.json b/apps/explorer/priv/contracts_abi/signet/weth.json deleted file mode 100644 index e6c8ff5adf85..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/weth.json +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - "type": "function", - "name": "deposit", - "inputs": [], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "withdraw", - "inputs": [ - { - "name": "wad", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "Deposit", - "inputs": [ - { - "name": "dst", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "wad", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Withdrawal", - "inputs": [ - { - "name": "src", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "wad", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - } -] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/signet/zenith.json b/apps/explorer/priv/contracts_abi/signet/zenith.json deleted file mode 100644 index c75b02cb3633..000000000000 --- a/apps/explorer/priv/contracts_abi/signet/zenith.json +++ /dev/null @@ -1,291 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_sequencerAdmin", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "addSequencer", - "inputs": [ - { - "name": "sequencer", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "blockCommitment", - "inputs": [ - { - "name": "header", - "type": "tuple", - "internalType": "struct Zenith.BlockHeader", - "components": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "hostBlockNumber", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gasLimit", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rewardAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "blockDataHash", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ], - "outputs": [ - { - "name": "commit", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "deployBlockNumber", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isSequencer", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "lastSubmittedAtBlock", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "removeSequencer", - "inputs": [ - { - "name": "sequencer", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "sequencerAdmin", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "submitBlock", - "inputs": [ - { - "name": "header", - "type": "tuple", - "internalType": "struct Zenith.BlockHeader", - "components": [ - { - "name": "rollupChainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "hostBlockNumber", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gasLimit", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rewardAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "blockDataHash", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "v", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "r", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "s", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "BlockSubmitted", - "inputs": [ - { - "name": "sequencer", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "rollupChainId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "gasLimit", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "rewardAddress", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "blockDataHash", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "SequencerSet", - "inputs": [ - { - "name": "sequencer", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "permissioned", - "type": "bool", - "indexed": true, - "internalType": "bool" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "BadSignature", - "inputs": [ - { - "name": "derivedSequencer", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "IncorrectHostBlock", - "inputs": [] - }, - { - "type": "error", - "name": "OneRollupBlockPerHostBlock", - "inputs": [] - }, - { - "type": "error", - "name": "OnlySequencerAdmin", - "inputs": [] - } -] \ No newline at end of file diff --git a/apps/indexer/lib/indexer/fetcher/signet/README.md b/apps/indexer/lib/indexer/fetcher/signet/README.md deleted file mode 100644 index 719fb51832ad..000000000000 --- a/apps/indexer/lib/indexer/fetcher/signet/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Signet Orders Fetcher - -This module indexes Order and Filled events from Signet's cross-chain order protocol. - -## Overview - -The Signet protocol enables cross-chain orders between a rollup (L2) and its host chain (L1). This fetcher: - -1. **Parses Order events** from the RollupOrders contract on L2 -2. **Parses Filled events** from both RollupOrders (L2) and HostOrders (L1) contracts -3. **Stores events independently** for querying and analytics -4. **Handles chain reorgs** gracefully by removing invalidated data - -**Note:** Orders and fills are indexed independently. Direct correlation between orders -and their corresponding fills is not possible at the indexer level — only block-level -coordination is available. This is a protocol-level constraint. - -## Event Types - -### RollupOrders Contract (L2) - -- `Order(uint256 deadline, Input[] inputs, Output[] outputs)` - New order created -- `Filled(Output[] outputs)` - Order filled on rollup -- `Sweep(address recipient, address token, uint256 amount)` - Remaining funds swept - -### HostOrders Contract (L1) - -- `Filled(Output[] outputs)` - Order filled on host chain - -## Data Structures - -**Input:** `(address token, uint256 amount)` -**Output:** `(address token, uint256 amount, address recipient, uint32 chainId)` - -## Configuration - -Add to your config: - -```elixir -config :indexer, Indexer.Fetcher.Signet.OrdersFetcher, - enabled: true, - rollup_orders_address: "0x...", # RollupOrders contract on L2 - host_orders_address: "0x...", # HostOrders contract on L1 (optional) - l1_rpc: "https://...", # L1 RPC endpoint (optional, for host fills) - l1_rpc_block_range: 1000, # Max blocks to fetch per L1 request - recheck_interval: 15_000, # Milliseconds between checks - start_block: 0 # Starting block for indexing -``` - -## Database Tables - -### signet_orders - -Stores Order events with their inputs, outputs, and any associated Sweep data. - -| Column | Type | Description | -|--------|------|-------------| -| transaction_hash | bytea | Primary key (part 1), transaction containing the order | -| log_index | integer | Primary key (part 2), log index within transaction | -| deadline | bigint | Order deadline timestamp | -| block_number | bigint | Block where order was created | -| inputs_json | text | JSON array of inputs | -| outputs_json | text | JSON array of outputs (includes chainId) | -| sweep_recipient | bytea | Sweep recipient (if any) | -| sweep_token | bytea | Sweep token (if any) | -| sweep_amount | numeric | Sweep amount (if any) | - -### signet_fills - -Stores Filled events from both chains. - -| Column | Type | Description | -|--------|------|-------------| -| chain_type | enum | Primary key (part 1), 'rollup' or 'host' | -| transaction_hash | bytea | Primary key (part 2), transaction containing the fill | -| log_index | integer | Primary key (part 3), log index within transaction | -| block_number | bigint | Block where fill occurred | -| outputs_json | text | JSON array of filled outputs (includes chainId) | - -## chainId Semantics - -The Output struct includes a `chainId` field with different semantics depending on context: - -- **In Order events (origin chain):** `chainId` is the **destination chain** where assets should be delivered -- **In Filled events (destination chain):** `chainId` is the **origin chain** where the order was created - -This semantic difference is inherent to the protocol and must be considered when interpreting the data. - -## Reorg Handling - -When a chain reorganization is detected: - -1. **Rollup reorg**: Deletes all orders and rollup fills from the reorg block onward -2. **Host reorg**: Deletes only host fills from the reorg block onward - -The fetcher will re-process the affected blocks after cleanup. - -## Metrics - -The fetcher logs: -- Number of orders/fills processed per batch -- Block ranges being indexed -- Any parsing or import errors - -## Files - -- `orders_fetcher.ex` - Main fetcher module -- `event_parser.ex` - ABI decoding and witness hash computation -- `reorg_handler.ex` - Chain reorganization handling -- `utils/db.ex` - Database query utilities -- `orders_fetcher/supervisor.ex` - Supervisor for the fetcher - -## Migration - -Run the migration to create tables: - -```bash -mix ecto.migrate --migrations-path apps/explorer/priv/signet/migrations -``` diff --git a/apps/indexer/lib/indexer/fetcher/signet/abi.ex b/apps/indexer/lib/indexer/fetcher/signet/abi.ex index 32b84ab8a080..5c7660788669 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/abi.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/abi.ex @@ -1,14 +1,8 @@ defmodule Indexer.Fetcher.Signet.Abi do @moduledoc """ - ABI definitions for Signet contracts. + ABI event topic hashes for Signet contracts. - ABIs are sourced from @signet-sh/sdk npm package and stored as JSON files - in apps/explorer/priv/contracts_abi/signet/. - - To update ABIs: - cd tools/signet-sdk && npm run extract - - ## Event Signatures (from SDK) + ## Event Signatures RollupOrders contract: - Order(uint256 deadline, (address token, uint256 amount)[] inputs, (address token, uint256 amount, address recipient, uint32 chainId)[] outputs) @@ -19,114 +13,43 @@ defmodule Indexer.Fetcher.Signet.Abi do - Filled((address token, uint256 amount, address recipient, uint32 chainId)[] outputs) """ - require Logger - - # Compute event topic hashes at compile time - # These match the event signatures from @signet-sh/sdk rollupOrdersAbi - - # Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) @order_event_signature "Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[])" - - # Filled((address,uint256,address,uint32)[]) @filled_event_signature "Filled((address,uint256,address,uint32)[])" - - # Sweep(address,address,uint256) - recipient and token are indexed @sweep_event_signature "Sweep(address,address,uint256)" - @doc """ - Returns the keccak256 topic hash for the Order event. - """ - @spec order_event_topic() :: binary() - def order_event_topic do - "0x" <> Base.encode16(ExKeccak.hash_256(@order_event_signature), case: :lower) - end - - @doc """ - Returns the keccak256 topic hash for the Filled event. - """ - @spec filled_event_topic() :: binary() - def filled_event_topic do - "0x" <> Base.encode16(ExKeccak.hash_256(@filled_event_signature), case: :lower) - end + @order_event_topic "0x" <> Base.encode16(ExKeccak.hash_256(@order_event_signature), case: :lower) + @filled_event_topic "0x" <> Base.encode16(ExKeccak.hash_256(@filled_event_signature), case: :lower) + @sweep_event_topic "0x" <> Base.encode16(ExKeccak.hash_256(@sweep_event_signature), case: :lower) - @doc """ - Returns the keccak256 topic hash for the Sweep event. - """ - @spec sweep_event_topic() :: binary() - def sweep_event_topic do - "0x" <> Base.encode16(ExKeccak.hash_256(@sweep_event_signature), case: :lower) - end + @doc "Returns the keccak256 topic hash for the Order event." + @spec order_event_topic() :: String.t() + def order_event_topic, do: @order_event_topic - @doc """ - Returns all event topics for the RollupOrders contract. - """ - @spec rollup_orders_event_topics() :: [binary()] - def rollup_orders_event_topics do - [order_event_topic(), filled_event_topic(), sweep_event_topic()] - end + @doc "Returns the keccak256 topic hash for the Filled event." + @spec filled_event_topic() :: String.t() + def filled_event_topic, do: @filled_event_topic - @doc """ - Returns all event topics for the HostOrders contract. - Only the Filled event is relevant from the host chain. - """ - @spec host_orders_event_topics() :: [binary()] - def host_orders_event_topics do - [filled_event_topic()] - end + @doc "Returns the keccak256 topic hash for the Sweep event." + @spec sweep_event_topic() :: String.t() + def sweep_event_topic, do: @sweep_event_topic - @doc """ - Load a Signet contract ABI from the priv directory. + @doc "Returns all event topics for the RollupOrders contract." + @spec rollup_orders_event_topics() :: [String.t()] + def rollup_orders_event_topics, do: [@order_event_topic, @filled_event_topic, @sweep_event_topic] - ## Examples + @doc "Returns all event topics for the HostOrders contract." + @spec host_orders_event_topics() :: [String.t()] + def host_orders_event_topics, do: [@filled_event_topic] - iex> Abi.load_abi("rollup_orders") - {:ok, [...]} - - iex> Abi.load_abi("nonexistent") - {:error, :not_found} - """ - @spec load_abi(String.t()) :: {:ok, list()} | {:error, atom()} - def load_abi(contract_name) do - path = abi_path(contract_name) - - case File.read(path) do - {:ok, content} -> - {:ok, Jason.decode!(content)} - - {:error, :enoent} -> - Logger.warning("Signet ABI not found: #{path}") - {:error, :not_found} - - {:error, reason} -> - Logger.error("Failed to load Signet ABI #{contract_name}: #{inspect(reason)}") - {:error, reason} - end - end - - @doc """ - Get the file path for a Signet contract ABI. - """ - @spec abi_path(String.t()) :: String.t() - def abi_path(contract_name) do - :explorer - |> Application.app_dir("priv/contracts_abi/signet/#{contract_name}.json") - end - - @doc """ - Returns the event signature string for the Order event. - """ + @doc "Returns the event signature string for the Order event." @spec order_event_signature() :: String.t() def order_event_signature, do: @order_event_signature - @doc """ - Returns the event signature string for the Filled event. - """ + @doc "Returns the event signature string for the Filled event." @spec filled_event_signature() :: String.t() def filled_event_signature, do: @filled_event_signature - @doc """ - Returns the event signature string for the Sweep event. - """ + @doc "Returns the event signature string for the Sweep event." @spec sweep_event_signature() :: String.t() def sweep_event_signature, do: @sweep_event_signature end diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index bc26f25af642..954dfdde9093 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -54,7 +54,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do alias Explorer.Chain alias Explorer.Chain.Signet.{Fill, Order} alias Indexer.BufferedTask - alias Indexer.Fetcher.Signet.{Abi, EventParser, ReorgHandler} + alias Indexer.Fetcher.Signet.{Abi, EventParser} alias Indexer.Helper, as: IndexerHelper @behaviour BufferedTask @@ -454,14 +454,4 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do :ok end - @doc """ - Handle chain reorganization by removing events from invalidated blocks. - - Called when a reorg is detected to clean up data from blocks that are - no longer in the canonical chain. - """ - @spec handle_reorg(non_neg_integer(), :rollup | :host) :: :ok - def handle_reorg(from_block, chain_type) do - ReorgHandler.handle_reorg(from_block, chain_type) - end end diff --git a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex b/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex deleted file mode 100644 index 9bbc42c07de1..000000000000 --- a/apps/indexer/lib/indexer/fetcher/signet/reorg_handler.ex +++ /dev/null @@ -1,113 +0,0 @@ -defmodule Indexer.Fetcher.Signet.ReorgHandler do - @moduledoc """ - Handles chain reorganizations for Signet order and fill data. - - When a chain reorg is detected, this module removes all orders and fills - from blocks that are no longer in the canonical chain, allowing them to - be re-indexed from the new canonical blocks. - """ - - require Logger - - import Ecto.Query - - alias Explorer.Chain.Signet.{Fill, Order} - alias Explorer.Repo - - @doc """ - Handle a chain reorganization starting from the given block number. - - Deletes all orders and fills at or after the reorg block, allowing - the fetcher to re-process these blocks. - - ## Parameters - - from_block: The block number where the reorg was detected - - chain_type: :rollup for L2 reorgs (affects orders and rollup fills), - :host for L1 reorgs (affects host fills only) - - ## Returns - - :ok - """ - @spec handle_reorg(non_neg_integer(), :rollup | :host) :: :ok - def handle_reorg(from_block, chain_type) do - Logger.info("Handling #{chain_type} chain reorg from block #{from_block}") - - case chain_type do - :rollup -> - handle_rollup_reorg(from_block) - - :host -> - handle_host_reorg(from_block) - end - - :ok - end - - # Handle reorg on the rollup (L2) chain - # This affects both orders and rollup fills - defp handle_rollup_reorg(from_block) do - # Wrap in transaction to ensure atomicity - Repo.transaction(fn -> - # Delete orders at or after the reorg block - {deleted_orders, _} = - Repo.delete_all( - from(o in Order, - where: o.block_number >= ^from_block - ) - ) - - # Delete rollup fills at or after the reorg block - {deleted_fills, _} = - Repo.delete_all( - from(f in Fill, - where: f.chain_type == :rollup and f.block_number >= ^from_block - ) - ) - - Logger.info( - "Rollup reorg cleanup: deleted #{deleted_orders} orders, #{deleted_fills} fills from block #{from_block}" - ) - end) - end - - # Handle reorg on the host (L1) chain - # This only affects host fills - defp handle_host_reorg(from_block) do - {deleted_fills, _} = - Repo.delete_all( - from(f in Fill, - where: f.chain_type == :host and f.block_number >= ^from_block - ) - ) - - Logger.info("Host reorg cleanup: deleted #{deleted_fills} fills from block #{from_block}") - end - - @doc """ - Check if a block is still valid in the chain by comparing its hash. - - Returns true if the block is still valid, false if it has been reorganized. - """ - @spec block_still_valid?(non_neg_integer(), binary(), keyword()) :: boolean() - def block_still_valid?(block_number, expected_hash, json_rpc_named_arguments) do - request = %{ - id: 1, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["0x#{Integer.to_string(block_number, 16)}", false] - } - - case EthereumJSONRPC.json_rpc(request, json_rpc_named_arguments) do - {:ok, block} when not is_nil(block) -> - actual_hash = Map.get(block, "hash") - normalize_hash(actual_hash) == normalize_hash(expected_hash) - - _ -> - false - end - end - - defp normalize_hash("0x" <> hex), do: String.downcase(hex) - defp normalize_hash(hex) when is_binary(hex), do: String.downcase(hex) - defp normalize_hash(_), do: nil -end diff --git a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex b/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex deleted file mode 100644 index ee3c38efad75..000000000000 --- a/apps/indexer/lib/indexer/fetcher/signet/utils/db.ex +++ /dev/null @@ -1,129 +0,0 @@ -defmodule Indexer.Fetcher.Signet.Utils.Db do - @moduledoc """ - Database utility functions for Signet order indexing. - """ - - import Ecto.Query - - alias Explorer.Chain.Signet.{Fill, Order} - alias Explorer.Repo - - @doc """ - Get the highest indexed block number for orders. - Returns the default if no orders exist. - """ - @spec highest_indexed_order_block(non_neg_integer()) :: non_neg_integer() - def highest_indexed_order_block(default \\ 0) do - case Repo.one(from(o in Order, select: max(o.block_number))) do - nil -> default - block -> block - end - end - - @doc """ - Get the highest indexed block number for fills on a specific chain. - Returns the default if no fills exist. - """ - @spec highest_indexed_fill_block(:rollup | :host, non_neg_integer()) :: non_neg_integer() - def highest_indexed_fill_block(chain_type, default \\ 0) do - case Repo.one( - from(f in Fill, - where: f.chain_type == ^chain_type, - select: max(f.block_number) - ) - ) do - nil -> default - block -> block - end - end - - @doc """ - Get an order by its transaction hash and log index. - """ - @spec get_order_by_tx_and_log(binary(), non_neg_integer()) :: Order.t() | nil - def get_order_by_tx_and_log(transaction_hash, log_index) do - Repo.one( - from(o in Order, - where: o.transaction_hash == ^transaction_hash and o.log_index == ^log_index - ) - ) - end - - @doc """ - Get all orders for a specific transaction. - """ - @spec get_orders_for_transaction(binary()) :: [Order.t()] - def get_orders_for_transaction(transaction_hash) do - Repo.all( - from(o in Order, - where: o.transaction_hash == ^transaction_hash, - order_by: [asc: o.log_index] - ) - ) - end - - @doc """ - Get a fill by its composite primary key. - """ - @spec get_fill(atom(), binary(), non_neg_integer()) :: Fill.t() | nil - def get_fill(chain_type, transaction_hash, log_index) do - Repo.one( - from(f in Fill, - where: - f.chain_type == ^chain_type and - f.transaction_hash == ^transaction_hash and - f.log_index == ^log_index - ) - ) - end - - @doc """ - Get all fills for a specific transaction. - """ - @spec get_fills_for_transaction(binary()) :: [Fill.t()] - def get_fills_for_transaction(transaction_hash) do - Repo.all( - from(f in Fill, - where: f.transaction_hash == ^transaction_hash, - order_by: [asc: f.chain_type, asc: f.log_index] - ) - ) - end - - @doc """ - Get orders by deadline range for monitoring. - """ - @spec get_orders_by_deadline_range(non_neg_integer(), non_neg_integer()) :: [Order.t()] - def get_orders_by_deadline_range(from_deadline, to_deadline) do - Repo.all( - from(o in Order, - where: o.deadline >= ^from_deadline and o.deadline <= ^to_deadline, - order_by: [asc: o.deadline] - ) - ) - end - - @doc """ - Count orders and fills for metrics. - """ - @spec get_order_fill_counts() :: %{ - orders: non_neg_integer(), - rollup_fills: non_neg_integer(), - host_fills: non_neg_integer() - } - def get_order_fill_counts do - orders_count = Repo.one(from(o in Order, select: count(o.transaction_hash))) - - rollup_fills_count = - Repo.one(from(f in Fill, where: f.chain_type == :rollup, select: count(f.transaction_hash))) - - host_fills_count = - Repo.one(from(f in Fill, where: f.chain_type == :host, select: count(f.transaction_hash))) - - %{ - orders: orders_count || 0, - rollup_fills: rollup_fills_count || 0, - host_fills: host_fills_count || 0 - } - end -end diff --git a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs index c738289e49db..7b19ebcc62db 100644 --- a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs @@ -1,8 +1,6 @@ defmodule Indexer.Fetcher.Signet.AbiTest do @moduledoc """ Unit tests for Indexer.Fetcher.Signet.Abi module. - - Tests verify event topic hash computation and ABI loading functionality. """ use ExUnit.Case, async: true @@ -10,38 +8,6 @@ defmodule Indexer.Fetcher.Signet.AbiTest do alias Indexer.Fetcher.Signet.Abi describe "event topic hashes" do - test "order_event_topic/0 returns valid keccak256 hash" do - topic = Abi.order_event_topic() - - # Should be a hex string starting with 0x and 64 hex chars (32 bytes) - assert String.starts_with?(topic, "0x") - assert String.length(topic) == 66 - - # Verify it's a valid hex string - "0x" <> hex = topic - assert {:ok, _} = Base.decode16(hex, case: :lower) - end - - test "filled_event_topic/0 returns valid keccak256 hash" do - topic = Abi.filled_event_topic() - - assert String.starts_with?(topic, "0x") - assert String.length(topic) == 66 - - "0x" <> hex = topic - assert {:ok, _} = Base.decode16(hex, case: :lower) - end - - test "sweep_event_topic/0 returns valid keccak256 hash" do - topic = Abi.sweep_event_topic() - - assert String.starts_with?(topic, "0x") - assert String.length(topic) == 66 - - "0x" <> hex = topic - assert {:ok, _} = Base.decode16(hex, case: :lower) - end - test "event topics are different from each other" do order_topic = Abi.order_event_topic() filled_topic = Abi.filled_event_topic() @@ -51,53 +17,13 @@ defmodule Indexer.Fetcher.Signet.AbiTest do refute order_topic == sweep_topic refute filled_topic == sweep_topic end - - test "event topics are consistent (deterministic)" do - # Topics should be the same on repeated calls - topic1 = Abi.order_event_topic() - topic2 = Abi.order_event_topic() - assert topic1 == topic2 - end - end - - describe "event signatures" do - test "order_event_signature/0 returns expected format" do - sig = Abi.order_event_signature() - - # Should follow format: Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) - assert String.starts_with?(sig, "Order(") - assert String.contains?(sig, "uint256") - assert String.contains?(sig, "address") - end - - test "filled_event_signature/0 returns expected format" do - sig = Abi.filled_event_signature() - - # Should follow format: Filled((address,uint256,address,uint32)[]) - assert String.starts_with?(sig, "Filled(") - assert String.contains?(sig, "uint32") - end - - test "sweep_event_signature/0 returns expected format" do - sig = Abi.sweep_event_signature() - - # Should follow format: Sweep(address,address,uint256) - assert String.starts_with?(sig, "Sweep(") - assert String.contains?(sig, "address") - end end describe "rollup_orders_event_topics/0" do test "returns list of three topics" do topics = Abi.rollup_orders_event_topics() - assert is_list(topics) assert length(topics) == 3 - end - - test "includes all rollup event topics" do - topics = Abi.rollup_orders_event_topics() - assert Abi.order_event_topic() in topics assert Abi.filled_event_topic() in topics assert Abi.sweep_event_topic() in topics @@ -108,47 +34,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do test "returns list with only filled topic" do topics = Abi.host_orders_event_topics() - assert is_list(topics) - assert length(topics) == 1 - assert Abi.filled_event_topic() in topics - end - end - - describe "abi_path/1" do - test "returns path for rollup_orders contract" do - path = Abi.abi_path("rollup_orders") - - assert String.contains?(path, "priv/contracts_abi/signet/rollup_orders.json") - end - - test "returns path for host_orders contract" do - path = Abi.abi_path("host_orders") - - assert String.contains?(path, "priv/contracts_abi/signet/host_orders.json") - end - end - - describe "load_abi/1" do - test "loads rollup_orders ABI successfully" do - result = Abi.load_abi("rollup_orders") - - assert {:ok, abi} = result - assert is_list(abi) - assert length(abi) > 0 - end - - test "loads host_orders ABI successfully" do - result = Abi.load_abi("host_orders") - - assert {:ok, abi} = result - assert is_list(abi) - assert length(abi) > 0 - end - - test "returns error for nonexistent contract" do - result = Abi.load_abi("nonexistent_contract") - - assert {:error, :not_found} = result + assert topics == [Abi.filled_event_topic()] end end end diff --git a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs deleted file mode 100644 index 3248e8546dab..000000000000 --- a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs +++ /dev/null @@ -1,183 +0,0 @@ -defmodule Indexer.Fetcher.Signet.EventParserTest do - @moduledoc """ - Unit tests for Indexer.Fetcher.Signet.EventParser module. - - Tests verify event parsing and Output struct field ordering. - - Output struct field order (per @signet-sh/sdk): - (address token, uint256 amount, address recipient, uint32 chainId) - - Note: Orders and fills are indexed independently - no correlation between them. - """ - - use ExUnit.Case, async: true - - alias Indexer.Fetcher.Signet.{Abi, EventParser} - - # Test addresses (20 bytes each) - @test_token <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>> - @test_recipient <<21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40>> - # 1e18 - @test_amount 1_000_000_000_000_000_000 - # Mainnet - @test_chain_id 1 - - describe "parse_rollup_logs/1" do - test "returns empty lists for empty logs" do - {:ok, {orders, fills}} = EventParser.parse_rollup_logs([]) - - assert orders == [] - assert fills == [] - end - - test "ignores logs with non-matching topics" do - logs = [ - %{ - "topics" => ["0x0000000000000000000000000000000000000000000000000000000000000000"], - "data" => "0x", - "blockNumber" => "0x1", - "transactionHash" => "0x" <> String.duplicate("ab", 32), - "logIndex" => "0x0" - } - ] - - {:ok, {orders, fills}} = EventParser.parse_rollup_logs(logs) - - assert orders == [] - assert fills == [] - end - end - - describe "parse_host_filled_logs/1" do - test "returns empty list for empty logs" do - {:ok, fills} = EventParser.parse_host_filled_logs([]) - - assert fills == [] - end - - test "ignores logs with non-matching topics" do - logs = [ - %{ - "topics" => ["0x0000000000000000000000000000000000000000000000000000000000000000"], - "data" => "0x", - "blockNumber" => "0x1", - "transactionHash" => "0x" <> String.duplicate("ab", 32), - "logIndex" => "0x0" - } - ] - - {:ok, fills} = EventParser.parse_host_filled_logs(logs) - - assert fills == [] - end - end - - describe "output struct field order verification" do - @tag :output_field_order - test "Output struct follows SDK order: (token, amount, recipient, chainId)" do - # Per @signet-sh/sdk, Output is defined as: - # struct Output { - # address token; - # uint256 amount; - # address recipient; - # uint32 chainId; - # } - # - # This is the correct field order used in the EventParser tuple: - # {token, amount, recipient, chain_id} - - token = @test_token - amount = @test_amount - recipient = @test_recipient - chain_id = @test_chain_id - - # The tuple format used in EventParser - output_tuple = {token, amount, recipient, chain_id} - - # Extract fields in the expected SDK order - {extracted_token, extracted_amount, extracted_recipient, extracted_chain_id} = output_tuple - - assert extracted_token == token - assert extracted_amount == amount - assert extracted_recipient == recipient - assert extracted_chain_id == chain_id - end - end - - describe "log field parsing helpers" do - test "handles hex-encoded block numbers" do - # Test that block_number parsing works for hex strings - log = %{ - # 16 in decimal - "blockNumber" => "0x10", - "topics" => [], - "data" => "0x" - } - - # The parser should correctly decode hex block numbers - # This is implicitly tested through parse_rollup_logs/1 - {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) - end - - test "handles integer block numbers" do - log = %{ - :block_number => 16, - :topics => [], - :data => "" - } - - {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) - end - end - - describe "event topic matching" do - test "Order event topic matches Abi module" do - order_topic = Abi.order_event_topic() - - # Verify the topic format - assert String.starts_with?(order_topic, "0x") - assert String.length(order_topic) == 66 - end - - test "Filled event topic matches Abi module" do - filled_topic = Abi.filled_event_topic() - - assert String.starts_with?(filled_topic, "0x") - assert String.length(filled_topic) == 66 - end - - test "Sweep event topic matches Abi module" do - sweep_topic = Abi.sweep_event_topic() - - assert String.starts_with?(sweep_topic, "0x") - assert String.length(sweep_topic) == 66 - end - end - - describe "parsed event structure" do - test "parsed orders contain expected fields" do - # Orders should have: deadline, block_number, transaction_hash, log_index, inputs_json, outputs_json - # Primary key is (transaction_hash, log_index) - expected_fields = ~w(deadline block_number transaction_hash log_index inputs_json outputs_json)a - - # Verify the expected_fields list is what we're looking for - assert :deadline in expected_fields - assert :block_number in expected_fields - assert :transaction_hash in expected_fields - assert :log_index in expected_fields - assert :inputs_json in expected_fields - assert :outputs_json in expected_fields - end - - test "parsed fills contain expected fields" do - # Fills should have: block_number, transaction_hash, log_index, outputs_json - # Primary key is (chain_type, transaction_hash, log_index) - chain_type added at import - expected_fields = ~w(block_number transaction_hash log_index outputs_json)a - - assert :block_number in expected_fields - assert :transaction_hash in expected_fields - assert :log_index in expected_fields - assert :outputs_json in expected_fields - end - end -end diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs index 5158cdd6cfbe..ca7792a37449 100644 --- a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -2,8 +2,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do @moduledoc """ Integration tests for the Signet OrdersFetcher module. - These tests verify the full pipeline from event fetching through - database insertion, including reorg handling and database utilities. + Tests verify the full pipeline from event fetching through + database insertion. Note: Orders and fills are indexed independently with no correlation. Primary keys are: @@ -16,10 +16,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do import Explorer.Factory alias Explorer.Chain - alias Explorer.Chain.Signet.{Order, Fill} - alias Explorer.Repo - alias Indexer.Fetcher.Signet.{OrdersFetcher, ReorgHandler} - alias Indexer.Fetcher.Signet.Utils.Db + alias Indexer.Fetcher.Signet.OrdersFetcher @moduletag :signet @@ -125,130 +122,6 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do end end - describe "ReorgHandler" do - test "rollup reorg removes orders and rollup fills from affected blocks" do - # Insert test data - insert_test_order(<<1::256>>, 100) - insert_test_order(<<2::256>>, 200) - insert_test_order(<<3::256>>, 300) - - insert_test_fill(:rollup, <<11::256>>, 150) - insert_test_fill(:rollup, <<12::256>>, 250) - # Host fill should survive rollup reorg - insert_test_fill(:host, <<13::256>>, 250) - - assert Repo.aggregate(Order, :count) == 3 - assert Repo.aggregate(Fill, :count) == 3 - - # Trigger reorg from block 200 - ReorgHandler.handle_reorg(200, :rollup) - - # Orders from block 200+ should be deleted - assert Repo.aggregate(Order, :count) == 1 - remaining_order = Repo.one(Order) - assert remaining_order.block_number == 100 - - # Rollup fills from block 200+ should be deleted - fills = Repo.all(Fill) - assert length(fills) == 2 - rollup_fills = Enum.filter(fills, &(&1.chain_type == :rollup)) - host_fills = Enum.filter(fills, &(&1.chain_type == :host)) - assert length(rollup_fills) == 1 - assert hd(rollup_fills).block_number == 150 - # Host fill should remain - assert length(host_fills) == 1 - end - - test "host reorg only removes host fills from affected blocks" do - # Insert test data - insert_test_order(<<1::256>>, 100) - insert_test_fill(:rollup, <<11::256>>, 150) - insert_test_fill(:host, <<21::256>>, 200) - insert_test_fill(:host, <<22::256>>, 300) - - # Trigger host reorg from block 250 - ReorgHandler.handle_reorg(250, :host) - - # Order should remain - assert Repo.aggregate(Order, :count) == 1 - - # Rollup fill should remain - fills = Repo.all(Fill) - rollup_fills = Enum.filter(fills, &(&1.chain_type == :rollup)) - host_fills = Enum.filter(fills, &(&1.chain_type == :host)) - - assert length(rollup_fills) == 1 - # Only host fill at block 200 remains - assert length(host_fills) == 1 - assert hd(host_fills).block_number == 200 - end - - test "reorg at genesis deletes all data" do - insert_test_order(<<1::256>>, 100) - insert_test_order(<<2::256>>, 200) - insert_test_fill(:rollup, <<11::256>>, 150) - insert_test_fill(:host, <<21::256>>, 250) - - ReorgHandler.handle_reorg(0, :rollup) - - assert Repo.aggregate(Order, :count) == 0 - rollup_fills = Repo.all(from(f in Fill, where: f.chain_type == :rollup)) - assert length(rollup_fills) == 0 - # Host fill should remain even in rollup reorg - host_fills = Repo.all(from(f in Fill, where: f.chain_type == :host)) - assert length(host_fills) == 1 - end - end - - describe "Db utility functions" do - test "highest_indexed_order_block returns correct value" do - assert Db.highest_indexed_order_block(0) == 0 - - insert_test_order(<<1::256>>, 100) - insert_test_order(<<2::256>>, 200) - insert_test_order(<<3::256>>, 150) - - assert Db.highest_indexed_order_block(0) == 200 - end - - test "highest_indexed_fill_block returns correct value per chain" do - assert Db.highest_indexed_fill_block(:rollup, 0) == 0 - assert Db.highest_indexed_fill_block(:host, 0) == 0 - - insert_test_fill(:rollup, <<11::256>>, 100) - insert_test_fill(:rollup, <<12::256>>, 200) - insert_test_fill(:host, <<21::256>>, 150) - insert_test_fill(:host, <<22::256>>, 300) - - assert Db.highest_indexed_fill_block(:rollup, 0) == 200 - assert Db.highest_indexed_fill_block(:host, 0) == 300 - end - - test "get_orders_by_deadline_range returns orders in range" do - insert_test_order_with_deadline(<<1::256>>, 100, 1_000) - insert_test_order_with_deadline(<<2::256>>, 200, 2_000) - insert_test_order_with_deadline(<<3::256>>, 300, 3_000) - - orders = Db.get_orders_by_deadline_range(1_500, 2_500) - assert length(orders) == 1 - assert hd(orders).deadline == 2_000 - end - - test "get_order_fill_counts returns accurate counts" do - insert_test_order(<<1::256>>, 100) - insert_test_order(<<2::256>>, 200) - insert_test_fill(:rollup, <<11::256>>, 150) - insert_test_fill(:rollup, <<12::256>>, 250) - insert_test_fill(:host, <<21::256>>, 300) - - counts = Db.get_order_fill_counts() - - assert counts.orders == 2 - assert counts.rollup_fills == 2 - assert counts.host_fills == 1 - end - end - describe "factory integration" do test "signet_order factory creates valid order" do order = insert(:signet_order) @@ -285,53 +158,4 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do assert fill.block_number == 123 end end - - # Helper functions for test data insertion - - defp insert_test_order(tx_hash, block_number) do - params = %{ - deadline: 1_700_000_000, - block_number: block_number, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0xabc", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0xdef", "recipient" => "0x123", "amount" => "500", "chainId" => "1"}]) - } - - {:ok, %{insert_signet_orders: [order]}} = - Chain.import(%{signet_orders: %{params: [params]}, timeout: :infinity}) - - order - end - - defp insert_test_order_with_deadline(tx_hash, block_number, deadline) do - params = %{ - deadline: deadline, - block_number: block_number, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0xabc", "amount" => "1000"}]), - outputs_json: Jason.encode!([%{"token" => "0xdef", "recipient" => "0x123", "amount" => "500", "chainId" => "1"}]) - } - - {:ok, %{insert_signet_orders: [order]}} = - Chain.import(%{signet_orders: %{params: [params]}, timeout: :infinity}) - - order - end - - defp insert_test_fill(chain_type, tx_hash, block_number) do - params = %{ - chain_type: chain_type, - block_number: block_number, - transaction_hash: tx_hash, - log_index: 0, - outputs_json: Jason.encode!([%{"token" => "0xfff", "recipient" => "0x999", "amount" => "500", "chainId" => "1"}]) - } - - {:ok, %{insert_signet_fills: [fill]}} = - Chain.import(%{signet_fills: %{params: [params]}, timeout: :infinity}) - - fill - end end diff --git a/tools/signet-sdk/README.md b/tools/signet-sdk/README.md deleted file mode 100644 index 416bf8a8ffcf..000000000000 --- a/tools/signet-sdk/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Signet SDK ABI Extractor - -This tool extracts ABI definitions from the `@signet-sh/sdk` npm package for use in the Elixir-based Blockscout indexer. - -## Overview - -Blockscout is an Elixir application, but the Signet protocol's canonical ABI definitions are maintained in the TypeScript SDK (`@signet-sh/sdk`). This tool bridges that gap by: - -1. Installing the SDK as an npm dependency -2. Extracting ABIs as JSON files -3. Storing them in `apps/explorer/priv/contracts_abi/signet/` - -The Elixir indexer then loads these JSON files via `Indexer.Fetcher.Signet.Abi`. - -## Usage - -### Initial Setup - -```bash -cd tools/signet-sdk -npm install -npm run extract -``` - -### Updating ABIs - -When the SDK is updated: - -1. Update the version in `package.json` -2. Run: - ```bash - npm install - npm run extract - ``` -3. Commit the updated JSON files in `apps/explorer/priv/contracts_abi/signet/` - -## Extracted ABIs - -The following ABIs are extracted from `@signet-sh/sdk`: - -| File | Contract | Description | -|------|----------|-------------| -| `rollup_orders.json` | RollupOrders | L2 order creation and fills | -| `host_orders.json` | HostOrders | L1 fills | -| `passage.json` | Passage | L1→L2 bridging | -| `rollup_passage.json` | RollupPassage | L2→L1 bridging | -| `permit2.json` | Permit2 | Gasless token approvals | -| `weth.json` | WETH | Wrapped ETH | -| `zenith.json` | Zenith | Block submission | -| `transactor.json` | Transactor | Cross-chain transactions | -| `bundle_helper.json` | BundleHelper | Bundle utilities | -| `events_index.json` | — | Index of all events | - -## Event Signatures - -Key events tracked by the indexer (from `rollup_orders.json`): - -- **Order**: `Order(uint256 deadline, (address,uint256)[] inputs, (address,uint256,address,uint32)[] outputs)` -- **Filled**: `Filled((address,uint256,address,uint32)[] outputs)` -- **Sweep**: `Sweep(address indexed recipient, address indexed token, uint256 amount)` - -## Architecture - -``` -@signet-sh/sdk (npm) - ↓ -tools/signet-sdk/extract-abis.mjs - ↓ -apps/explorer/priv/contracts_abi/signet/*.json - ↓ -Indexer.Fetcher.Signet.Abi (Elixir module) - ↓ -Indexer.Fetcher.Signet.EventParser -``` diff --git a/tools/signet-sdk/extract-abis.mjs b/tools/signet-sdk/extract-abis.mjs deleted file mode 100644 index b87b1d12926e..000000000000 --- a/tools/signet-sdk/extract-abis.mjs +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Extract ABIs from @signet-sh/sdk and save as JSON files for use in Elixir. - * - * Run with: npm run extract - * Output: ../../apps/explorer/priv/contracts_abi/signet/ - */ - -import { writeFileSync, mkdirSync } from 'fs'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -import { - rollupOrdersAbi, - hostOrdersAbi, - passageAbi, - rollupPassageAbi, - permit2Abi, - wethAbi, - zenithAbi, - transactorAbi, - bundleHelperAbi, -} from '@signet-sh/sdk'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const OUTPUT_DIR = join(__dirname, '../../apps/explorer/priv/contracts_abi/signet'); - -// Ensure output directory exists -mkdirSync(OUTPUT_DIR, { recursive: true }); - -const abis = { - 'rollup_orders': rollupOrdersAbi, - 'host_orders': hostOrdersAbi, - 'passage': passageAbi, - 'rollup_passage': rollupPassageAbi, - 'permit2': permit2Abi, - 'weth': wethAbi, - 'zenith': zenithAbi, - 'transactor': transactorAbi, - 'bundle_helper': bundleHelperAbi, -}; - -for (const [name, abi] of Object.entries(abis)) { - const outputPath = join(OUTPUT_DIR, `${name}.json`); - writeFileSync(outputPath, JSON.stringify(abi, null, 2)); - console.log(`Wrote ${outputPath}`); -} - -// Also create a combined file with event signatures for quick reference -const events = []; -for (const [name, abi] of Object.entries(abis)) { - for (const item of abi) { - if (item.type === 'event') { - events.push({ - contract: name, - name: item.name, - signature: `${item.name}(${item.inputs.map(i => i.type).join(',')})`, - inputs: item.inputs, - }); - } - } -} - -const eventsPath = join(OUTPUT_DIR, 'events_index.json'); -writeFileSync(eventsPath, JSON.stringify(events, null, 2)); -console.log(`Wrote ${eventsPath}`); - -console.log('\nExtraction complete!'); diff --git a/tools/signet-sdk/package-lock.json b/tools/signet-sdk/package-lock.json deleted file mode 100644 index 9d9925a478cc..000000000000 --- a/tools/signet-sdk/package-lock.json +++ /dev/null @@ -1,244 +0,0 @@ -{ - "name": "signet-sdk-extractor", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "signet-sdk-extractor", - "version": "1.0.0", - "dependencies": { - "@signet-sh/sdk": "0.4.4" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", - "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@signet-sh/sdk": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@signet-sh/sdk/-/sdk-0.4.4.tgz", - "integrity": "sha512-+MU8hhzG5I7IbKvern2jWMqi61PIRnJx7jfaAwo6rIM2Pg8EAE1pxPToiVcgeRHtbymSNjMxMrItAYChcmUqrQ==", - "license": "MIT OR Apache-2.0", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "viem": "^2.0.0" - } - }, - "node_modules/abitype": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", - "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT", - "peer": true - }, - "node_modules/isows": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", - "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peer": true, - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/ox": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.1.tgz", - "integrity": "sha512-uU0llpthaaw4UJoXlseCyBHmQ3bLrQmz9rRLIAUHqv46uHuae9SE+ukYBRIPVCnlEnHKuWjDUcDFHWx9gbGNoA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.2.3", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.1.tgz", - "integrity": "sha512-c5YPQR/VueqoPG09Tp1JBw2iItKVRGVI0YkWekquRDZw0ciNBhO3muu2QjO9xFelOXh18q3d/kLbW83B2Oxf0g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.2.3", - "isows": "1.0.7", - "ox": "0.12.1", - "ws": "8.18.3" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/tools/signet-sdk/package.json b/tools/signet-sdk/package.json deleted file mode 100644 index 9e8f317e5db5..000000000000 --- a/tools/signet-sdk/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "signet-sdk-extractor", - "version": "1.0.0", - "private": true, - "description": "Extract ABIs from @signet-sh/sdk for use in Blockscout", - "type": "module", - "scripts": { - "extract": "node extract-abis.mjs" - }, - "dependencies": { - "@signet-sh/sdk": "0.4.4" - } -} From 24029adc4bb3915ac32373bdd221362eae719dd1 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Feb 2026 12:57:19 -0500 Subject: [PATCH 13/24] fix: address bugs, add EventParser table tests, register Repo.Signet Bug fixes in EventParser: normalize log keys once at entry point, return error tuples from parse_block_number/parse_log_index instead of silently defaulting to 0, extract @order_header_size constant, warn on multiple sweeps per tx, rewrite parse_host_filled_logs as single pass. Bug fixes in OrdersFetcher: propagate Chain.import errors instead of crashing, use config.l2_rpc_block_range for historical backfill instead of hardcoded 1000, add guards and pattern matching to get_latest_block. Fix changeset return specs in Order and Fill schemas. Register Explorer.Repo.Signet across all config files, repo.ex, and application.ex. Add "signet" to supported chain identities so CHAIN_TYPE=signet enables the signet import runners and migrations. Add 27 table-driven EventParser tests with independent ABI encoding helpers that cross-validate the manual binary decoder. Fix existing runner and fetcher tests to use properly cast Hash/Wei types. Add docker-compose.test.yml for local test postgres. Co-Authored-By: Claude Opus 4.6 --- apps/explorer/config/dev.exs | 3 +- apps/explorer/config/prod.exs | 3 +- apps/explorer/config/test.exs | 3 +- apps/explorer/lib/explorer/application.ex | 3 +- .../lib/explorer/chain/signet/fill.ex | 2 +- .../lib/explorer/chain/signet/order.ex | 2 +- apps/explorer/lib/explorer/repo.ex | 3 +- .../chain/import/runner/signet/fills_test.exs | 35 +- .../import/runner/signet/orders_test.exs | 25 +- .../indexer/fetcher/signet/event_parser.ex | 336 +++++------ .../indexer/fetcher/signet/orders_fetcher.ex | 38 +- .../fetcher/signet/event_parser_test.exs | 539 ++++++++++++++++++ .../fetcher/signet/orders_fetcher_test.exs | 14 +- config/config_helper.exs | 4 +- config/runtime/dev.exs | 3 +- config/runtime/prod.exs | 3 +- docker-compose.test.yml | 17 + 17 files changed, 784 insertions(+), 249 deletions(-) create mode 100644 apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs create mode 100644 docker-compose.test.yml diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index 715741583d1a..6dc674ac6326 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -32,7 +32,8 @@ for repo <- [ Explorer.Repo.Suave, Explorer.Repo.Zilliqa, Explorer.Repo.ZkSync, - Explorer.Repo.Neon + Explorer.Repo.Neon, + Explorer.Repo.Signet ] do config :explorer, repo, timeout: :timer.seconds(80) end diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index 8371ee6c6d7e..4f0f47e472d0 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -34,7 +34,8 @@ for repo <- [ Explorer.Repo.Suave, Explorer.Repo.Zilliqa, Explorer.Repo.ZkSync, - Explorer.Repo.Neon + Explorer.Repo.Neon, + Explorer.Repo.Signet ] do config :explorer, repo, prepare: :unnamed, diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 2de39c4c3e2e..e8b3d25b6b22 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -84,7 +84,8 @@ for repo <- [ Explorer.Repo.Suave, Explorer.Repo.Zilliqa, Explorer.Repo.ZkSync, - Explorer.Repo.Neon + Explorer.Repo.Neon, + Explorer.Repo.Signet ] do config :explorer, repo, database: database, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 375d9be6a185..947f570eaa35 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -378,7 +378,8 @@ defmodule Explorer.Application do Explorer.Repo.Stability, Explorer.Repo.Suave, Explorer.Repo.Zilliqa, - Explorer.Repo.ZkSync + Explorer.Repo.ZkSync, + Explorer.Repo.Signet ] else [] diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex index ccbfcba166d0..e56438b1192d 100644 --- a/apps/explorer/lib/explorer/chain/signet/fill.ex +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -56,7 +56,7 @@ defmodule Explorer.Chain.Signet.Fill do @doc """ Validates that the `attrs` are valid. """ - @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = fill, attrs \\ %{}) do fill |> cast(attrs, @allowed_attrs) diff --git a/apps/explorer/lib/explorer/chain/signet/order.ex b/apps/explorer/lib/explorer/chain/signet/order.ex index 25468ddc7e55..28b43ee55183 100644 --- a/apps/explorer/lib/explorer/chain/signet/order.ex +++ b/apps/explorer/lib/explorer/chain/signet/order.ex @@ -68,7 +68,7 @@ defmodule Explorer.Chain.Signet.Order do @doc """ Validates that the `attrs` are valid. """ - @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = order, attrs \\ %{}) do order |> cast(attrs, @allowed_attrs) diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex index 17c445d7bca9..f6b634e18398 100644 --- a/apps/explorer/lib/explorer/repo.ex +++ b/apps/explorer/lib/explorer/repo.ex @@ -153,7 +153,8 @@ defmodule Explorer.Repo do Explorer.Repo.Suave, Explorer.Repo.Zilliqa, Explorer.Repo.ZkSync, - Explorer.Repo.Neon + Explorer.Repo.Neon, + Explorer.Repo.Signet ] do defmodule repo do use Ecto.Repo, diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs index 079e30111dd1..1831c869a20f 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -2,15 +2,21 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do use Explorer.DataCase alias Ecto.Multi + alias Explorer.Chain.Hash alias Explorer.Chain.Import.Runner.Signet.Fills, as: FillsRunner alias Explorer.Chain.Signet.Fill alias Explorer.Repo @moduletag :signet + defp cast_hash!(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + hash + end + describe "run/3" do test "inserts a new rollup fill" do - tx_hash = <<1::256>> + tx_hash = cast_hash!(<<1::256>>) params = [ %{ @@ -36,7 +42,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do end test "inserts a new host fill" do - tx_hash = <<2::256>> + tx_hash = cast_hash!(<<2::256>>) params = [ %{ @@ -59,7 +65,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do end test "same transaction can have fills on different chains" do - tx_hash = <<3::256>> + tx_hash = cast_hash!(<<3::256>>) rollup_params = [ %{ @@ -95,29 +101,16 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do # Should have two fills (one per chain type) assert Repo.aggregate(Fill, :count) == 2 - tx_hash_struct = %Explorer.Chain.Hash.Full{byte_count: 32, bytes: tx_hash} - # Verify both exist - rollup_fill = - Repo.get_by(Fill, - chain_type: :rollup, - transaction_hash: tx_hash_struct, - log_index: 0 - ) - - host_fill = - Repo.get_by(Fill, - chain_type: :host, - transaction_hash: tx_hash_struct, - log_index: 0 - ) + rollup_fill = Repo.get_by(Fill, chain_type: :rollup, transaction_hash: tx_hash, log_index: 0) + host_fill = Repo.get_by(Fill, chain_type: :host, transaction_hash: tx_hash, log_index: 0) assert rollup_fill.block_number == 100 assert host_fill.block_number == 200 end test "handles duplicate fills with upsert on composite primary key" do - tx_hash = <<4::256>> + tx_hash = cast_hash!(<<4::256>>) params1 = [ %{ @@ -160,7 +153,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do end test "different log_index creates separate fills" do - tx_hash = <<5::256>> + tx_hash = cast_hash!(<<5::256>>) params = [ %{ @@ -197,7 +190,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do %{ chain_type: if(rem(i, 2) == 0, do: :host, else: :rollup), block_number: 100 + i, - transaction_hash: <<100 + i::256>>, + transaction_hash: cast_hash!(<<100 + i::256>>), log_index: 0, outputs_json: Jason.encode!([ diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs index bbfd772607d4..dc0ab5a4acae 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -2,15 +2,21 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do use Explorer.DataCase alias Ecto.Multi + alias Explorer.Chain.Hash alias Explorer.Chain.Import.Runner.Signet.Orders, as: OrdersRunner alias Explorer.Chain.Signet.Order alias Explorer.Repo @moduletag :signet + defp cast_hash!(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + hash + end + describe "run/3" do test "inserts a new order" do - tx_hash = <<1::256>> + tx_hash = cast_hash!(<<1::256>>) params = [ %{ @@ -37,7 +43,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do end test "handles duplicate orders with upsert on composite primary key" do - tx_hash = <<2::256>> + tx_hash = cast_hash!(<<2::256>>) params = [ %{ @@ -80,14 +86,13 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do assert Repo.aggregate(Order, :count) == 1 # Order should be updated - tx_hash_struct = %Explorer.Chain.Hash.Full{byte_count: 32, bytes: tx_hash} - order = Repo.get_by(Order, transaction_hash: tx_hash_struct, log_index: 0) + order = Repo.get_by(Order, transaction_hash: tx_hash, log_index: 0) assert order.deadline == 1_700_000_001 assert order.block_number == 101 end test "different log_index creates separate orders" do - tx_hash = <<3::256>> + tx_hash = cast_hash!(<<3::256>>) params = [ %{ @@ -121,9 +126,9 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do end test "inserts order with sweep data" do - tx_hash = <<4::256>> - sweep_recipient = <<5::160>> - sweep_token = <<6::160>> + tx_hash = cast_hash!(<<4::256>>) + {:ok, sweep_recipient} = Explorer.Chain.Hash.Address.cast(<<5::160>>) + {:ok, sweep_token} = Explorer.Chain.Hash.Address.cast(<<6::160>>) params = [ %{ @@ -136,7 +141,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), sweep_recipient: sweep_recipient, sweep_token: sweep_token, - sweep_amount: Decimal.new("12345") + sweep_amount: %Explorer.Chain.Wei{value: Decimal.new("12345")} } ] @@ -154,7 +159,7 @@ defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do %{ deadline: 1_700_000_000 + i, block_number: 100 + i, - transaction_hash: <<100 + i::256>>, + transaction_hash: cast_hash!(<<100 + i::256>>), log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}]), outputs_json: diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index ba60e39f58c0..40624c98af07 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -37,6 +37,9 @@ defmodule Indexer.Fetcher.Signet.EventParser do alias Indexer.Fetcher.Signet.Abi + # Size of the Order event header: deadline (32) + inputs_offset (32) + outputs_offset (32) + @order_header_size 96 + @doc """ Parse logs from the RollupOrders contract. @@ -46,18 +49,61 @@ defmodule Indexer.Fetcher.Signet.EventParser do @spec parse_rollup_logs([map()]) :: {:ok, {[map()], [map()]}} def parse_rollup_logs(logs) when is_list(logs) do {orders, fills, sweeps} = - Enum.reduce(logs, {[], [], []}, fn log, acc -> - classify_and_parse_log(log, acc) - end) + logs + |> Enum.map(&normalize_log/1) + |> Enum.reduce({[], [], []}, &classify_and_parse_log/2) - # Associate sweeps with their corresponding orders by transaction hash orders_with_sweeps = associate_sweeps_with_orders(orders, sweeps) {:ok, {Enum.reverse(orders_with_sweeps), Enum.reverse(fills)}} end + @doc """ + Parse Filled events from the HostOrders contract. + + Returns {:ok, fills} where fills is a list of maps ready for database import. + """ + @spec parse_host_filled_logs([map()]) :: {:ok, [map()]} + def parse_host_filled_logs(logs) when is_list(logs) do + filled_topic = Abi.filled_event_topic() + + fills = + logs + |> Enum.map(&normalize_log/1) + |> Enum.reduce([], fn log, acc -> + if Enum.at(log.topics, 0) == filled_topic do + case parse_filled_event(log) do + {:ok, fill} -> + [fill | acc] + + {:error, reason} -> + Logger.warning("Failed to parse host Filled event: #{inspect(reason)}") + acc + end + else + acc + end + end) + |> Enum.reverse() + + {:ok, fills} + end + + # Normalize log keys from JSON-RPC string keys or Elixir atom keys into a + # consistent atom-keyed map. Called once at the entry point so all downstream + # functions work with a single format. + defp normalize_log(log) when is_map(log) do + %{ + topics: Map.get(log, "topics") || Map.get(log, :topics) || [], + data: Map.get(log, "data") || Map.get(log, :data) || "", + transaction_hash: Map.get(log, "transactionHash") || Map.get(log, :transaction_hash), + block_number: Map.get(log, "blockNumber") || Map.get(log, :block_number), + log_index: Map.get(log, "logIndex") || Map.get(log, :log_index) + } + end + defp classify_and_parse_log(log, {orders_acc, fills_acc, sweeps_acc}) do - topic = get_topic(log, 0) + topic = Enum.at(log.topics, 0) cond do topic == Abi.order_event_topic() -> @@ -88,107 +134,73 @@ defmodule Indexer.Fetcher.Signet.EventParser do {orders, fills, sweeps} end - @doc """ - Parse Filled events from the HostOrders contract. - - Returns {:ok, fills} where fills is a list of maps ready for database import. - """ - @spec parse_host_filled_logs([map()]) :: {:ok, [map()]} - def parse_host_filled_logs(logs) when is_list(logs) do - filled_topic = Abi.filled_event_topic() - - fills = - logs - |> Enum.filter(fn log -> get_topic(log, 0) == filled_topic end) - |> Enum.map(&parse_filled_event/1) - |> Enum.filter(fn - {:ok, _} -> - true - - {:error, reason} -> - Logger.warning("Failed to parse host Filled event: #{inspect(reason)}") - false - end) - |> Enum.map(fn {:ok, fill} -> fill end) - - {:ok, fills} - end - - # Parse Order event - # Order(uint256 deadline, Input[] inputs, Output[] outputs) + # Parse Order event: Order(uint256 deadline, Input[] inputs, Output[] outputs) defp parse_order_event(log) do - data = get_log_data(log) - - with {:ok, decoded} <- decode_order_data(data) do - {deadline, inputs, outputs} = decoded - - order = %{ - deadline: deadline, - block_number: parse_block_number(log), - transaction_hash: get_transaction_hash(log), - log_index: parse_log_index(log), - inputs_json: Jason.encode!(format_inputs(inputs)), - outputs_json: Jason.encode!(format_outputs(outputs)) - } - - {:ok, order} + data = decode_hex_data(log.data) + + with {:ok, {deadline, inputs, outputs}} <- decode_order_data(data), + {:ok, block_number} <- parse_block_number(log), + {:ok, log_index} <- parse_log_index(log) do + {:ok, + %{ + deadline: deadline, + block_number: block_number, + transaction_hash: format_transaction_hash(log.transaction_hash), + log_index: log_index, + inputs_json: Jason.encode!(format_inputs(inputs)), + outputs_json: Jason.encode!(format_outputs(outputs)) + }} end end - # Parse Filled event - # Filled(Output[] outputs) + # Parse Filled event: Filled(Output[] outputs) defp parse_filled_event(log) do - data = get_log_data(log) - - with {:ok, outputs} <- decode_filled_data(data) do - fill = %{ - block_number: parse_block_number(log), - transaction_hash: get_transaction_hash(log), - log_index: parse_log_index(log), - outputs_json: Jason.encode!(format_outputs(outputs)) - } - - {:ok, fill} + data = decode_hex_data(log.data) + + with {:ok, outputs} <- decode_filled_data(data), + {:ok, block_number} <- parse_block_number(log), + {:ok, log_index} <- parse_log_index(log) do + {:ok, + %{ + block_number: block_number, + transaction_hash: format_transaction_hash(log.transaction_hash), + log_index: log_index, + outputs_json: Jason.encode!(format_outputs(outputs)) + }} end end - # Parse Sweep event - # Sweep(address indexed recipient, address indexed token, uint256 amount) - # Note: recipient and token are indexed (in topics), amount is in data + # Parse Sweep event: Sweep(address indexed recipient, address indexed token, uint256 amount) defp parse_sweep_event(log) do - data = get_log_data(log) + data = decode_hex_data(log.data) with {:ok, amount} <- decode_sweep_data(data) do - # recipient is in topic[1], token is in topic[2] - recipient = get_indexed_address(log, 1) - token = get_indexed_address(log, 2) - - sweep = %{ - transaction_hash: get_transaction_hash(log), - recipient: recipient, - token: token, - amount: amount - } - - {:ok, sweep} + {:ok, + %{ + transaction_hash: format_transaction_hash(log.transaction_hash), + recipient: decode_indexed_address(Enum.at(log.topics, 1)), + token: decode_indexed_address(Enum.at(log.topics, 2)), + amount: amount + }} end end - # Decode Order event data - # Order(uint256 deadline, Input[] inputs, Output[] outputs) - # Input = (address token, uint256 amount) - # Output = (address token, uint256 amount, address recipient, uint32 chainId) - defp decode_order_data(data) when is_binary(data) do - # ABI decode: uint256, dynamic array offset, dynamic array offset + # ABI decoders + + defp decode_order_data(data) when is_binary(data) and byte_size(data) >= @order_header_size do <> = data - # Parse inputs array - offset is from start of data (after first 32 bytes) - inputs_data = binary_part(rest, inputs_offset - 96, byte_size(rest) - inputs_offset + 96) + # ABI offsets are from the start of the data payload; subtract the header + # size to get the position within `rest` (which starts after the header). + inputs_data = + binary_part(rest, inputs_offset - @order_header_size, byte_size(rest) - inputs_offset + @order_header_size) + inputs = decode_input_array(inputs_data) - # Parse outputs array - outputs_data = binary_part(rest, outputs_offset - 96, byte_size(rest) - outputs_offset + 96) + outputs_data = + binary_part(rest, outputs_offset - @order_header_size, byte_size(rest) - outputs_offset + @order_header_size) + outputs = decode_output_array(outputs_data) {:ok, {deadline, inputs, outputs}} @@ -200,12 +212,9 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_order_data(_), do: {:error, :invalid_data} - # Decode Filled event data - # Filled(Output[] outputs) - defp decode_filled_data(data) when is_binary(data) do + defp decode_filled_data(data) when is_binary(data) and byte_size(data) >= 32 do <<_offset::unsigned-big-integer-size(256), rest::binary>> = data - outputs = decode_output_array(rest) - {:ok, outputs} + {:ok, decode_output_array(rest)} rescue e -> Logger.error("Error decoding Filled data: #{inspect(e)}") @@ -214,21 +223,12 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp decode_filled_data(_), do: {:error, :invalid_data} - # Decode Sweep event data - # Only amount is in data (recipient and token are indexed) - defp decode_sweep_data(data) when is_binary(data) do - <> = data - {:ok, amount} - rescue - e -> - Logger.error("Error decoding Sweep data: #{inspect(e)}") - {:error, :decode_failed} - end + defp decode_sweep_data(<>), do: {:ok, amount} defp decode_sweep_data(_), do: {:error, :invalid_data} - # Decode array of Input tuples - # Input = (address token, uint256 amount) + # Array decoders + defp decode_input_array(<>) do decode_inputs(rest, length, []) end @@ -240,12 +240,9 @@ defmodule Indexer.Fetcher.Signet.EventParser do count, acc ) do - input = {token, amount} - decode_inputs(rest, count - 1, [input | acc]) + decode_inputs(rest, count - 1, [{token, amount} | acc]) end - # Decode array of Output tuples - # Output = (address token, uint256 amount, address recipient, uint32 chainId) defp decode_output_array(<>) do decode_outputs(rest, length, []) end @@ -259,23 +256,23 @@ defmodule Indexer.Fetcher.Signet.EventParser do count, acc ) do - # Output struct order: token, amount, recipient, chainId - output = {token, amount, recipient, chain_id} - decode_outputs(rest, count - 1, [output | acc]) + decode_outputs(rest, count - 1, [{token, amount, recipient, chain_id} | acc]) end - # Associate sweep events with their corresponding orders by transaction hash + # Sweep association + defp associate_sweeps_with_orders(orders, sweeps) do sweeps_by_tx = Enum.group_by(sweeps, & &1.transaction_hash) Enum.map(orders, fn order -> case Map.get(sweeps_by_tx, order.transaction_hash) do - [sweep | _] -> - Map.merge(order, %{ - sweep_recipient: sweep.recipient, - sweep_token: sweep.token, - sweep_amount: sweep.amount - }) + [sweep] -> + attach_sweep(order, sweep) + + [sweep | rest] -> + Logger.warning("Multiple sweeps (#{length(rest) + 1}) for tx #{order.transaction_hash}, using first") + + attach_sweep(order, sweep) _ -> order @@ -283,18 +280,22 @@ defmodule Indexer.Fetcher.Signet.EventParser do end) end - # Format inputs for JSON storage + defp attach_sweep(order, sweep) do + Map.merge(order, %{ + sweep_recipient: sweep.recipient, + sweep_token: sweep.token, + sweep_amount: sweep.amount + }) + end + + # Formatters + defp format_inputs(inputs) do Enum.map(inputs, fn {token, amount} -> - %{ - "token" => format_address(token), - "amount" => Integer.to_string(amount) - } + %{"token" => format_address(token), "amount" => Integer.to_string(amount)} end) end - # Format outputs for JSON storage - # Output = (token, amount, recipient, chainId) defp format_outputs(outputs) do Enum.map(outputs, fn {token, amount, recipient, chain_id} -> %{ @@ -310,79 +311,44 @@ defmodule Indexer.Fetcher.Signet.EventParser do "0x" <> Base.encode16(bytes, case: :lower) end - defp get_topic(log, index) do - topics = Map.get(log, "topics") || Map.get(log, :topics) || [] - Enum.at(topics, index) - end - - # Get an indexed address from topics (topics contain 32-byte padded addresses) - defp get_indexed_address(log, topic_index) do - topic = get_topic(log, topic_index) + defp format_transaction_hash("0x" <> _ = hash), do: hash + defp format_transaction_hash(bytes) when is_binary(bytes), do: "0x" <> Base.encode16(bytes, case: :lower) + defp format_transaction_hash(_), do: nil - case topic do - "0x" <> hex -> - # Take last 40 chars (20 bytes) of the 64-char hex string - address_hex = String.slice(hex, -40, 40) - Base.decode16!(address_hex, case: :mixed) + # Field parsers - bytes when is_binary(bytes) and byte_size(bytes) == 32 -> - # Take last 20 bytes - binary_part(bytes, 12, 20) + defp decode_hex_data("0x" <> hex), do: Base.decode16!(hex, case: :mixed) + defp decode_hex_data(raw) when is_binary(raw), do: raw + defp decode_hex_data(_), do: <<>> - _ -> - nil - end + defp decode_indexed_address("0x" <> hex) do + address_hex = String.slice(hex, -40, 40) + Base.decode16!(address_hex, case: :mixed) end - defp get_log_data(log) do - data = Map.get(log, "data") || Map.get(log, :data) || "" - - case data do - "0x" <> hex -> Base.decode16!(hex, case: :mixed) - hex when is_binary(hex) -> Base.decode16!(hex, case: :mixed) - _ -> "" - end + defp decode_indexed_address(bytes) when is_binary(bytes) and byte_size(bytes) == 32 do + binary_part(bytes, 12, 20) end - defp get_transaction_hash(log) do - hash = Map.get(log, "transactionHash") || Map.get(log, :transaction_hash) + defp decode_indexed_address(_), do: nil - case hash do - "0x" <> _ -> hash - bytes when is_binary(bytes) -> "0x" <> Base.encode16(bytes, case: :lower) - _ -> nil + defp parse_block_number(%{block_number: "0x" <> hex}) do + case Integer.parse(hex, 16) do + {num, ""} -> {:ok, num} + _ -> {:error, {:invalid_block_number, "0x" <> hex}} end end - defp parse_block_number(log) do - block = Map.get(log, "blockNumber") || Map.get(log, :block_number) - - case block do - "0x" <> hex -> - {num, ""} = Integer.parse(hex, 16) - num + defp parse_block_number(%{block_number: num}) when is_integer(num), do: {:ok, num} + defp parse_block_number(%{block_number: other}), do: {:error, {:invalid_block_number, other}} - num when is_integer(num) -> - num - - _ -> - 0 + defp parse_log_index(%{log_index: "0x" <> hex}) do + case Integer.parse(hex, 16) do + {num, ""} -> {:ok, num} + _ -> {:error, {:invalid_log_index, "0x" <> hex}} end end - defp parse_log_index(log) do - index = Map.get(log, "logIndex") || Map.get(log, :log_index) - - case index do - "0x" <> hex -> - {num, ""} = Integer.parse(hex, 16) - num - - num when is_integer(num) -> - num - - _ -> - 0 - end - end + defp parse_log_index(%{log_index: num}) when is_integer(num), do: {:ok, num} + defp parse_log_index(%{log_index: other}), do: {:error, {:invalid_log_index, other}} end diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 954dfdde9093..7caec013e18d 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -339,7 +339,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do if end_block <= 0 do {:ok, state, :done} else - start_block = max(0, end_block - 1000) + start_block = max(0, end_block - config.l2_rpc_block_range) with {:ok, logs} <- fetch_logs( @@ -392,9 +392,16 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do } case EthereumJSONRPC.json_rpc(request, json_rpc_named_arguments) do - {:ok, hex_block} -> - {block, ""} = Integer.parse(String.trim_leading(hex_block, "0x"), 16) - {:ok, block} + {:ok, hex_block} when is_binary(hex_block) -> + hex = String.trim_leading(hex_block, "0x") + + case Integer.parse(hex, 16) do + {block, ""} -> {:ok, block} + _ -> {:error, {:invalid_block_number, hex_block}} + end + + {:ok, unexpected} -> + {:error, {:unexpected_response, unexpected}} {:error, reason} -> {:error, reason} @@ -431,13 +438,10 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do defp import_orders([]), do: :ok defp import_orders(orders) do - {:ok, _} = - Chain.import(%{ - signet_orders: %{params: orders}, - timeout: :infinity - }) - - :ok + case Chain.import(%{signet_orders: %{params: orders}, timeout: :infinity}) do + {:ok, _} -> :ok + {:error, step, reason, _} -> {:error, {step, reason}} + end end defp import_fills([], _chain_type), do: :ok @@ -445,13 +449,9 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do defp import_fills(fills, chain_type) do fills_with_chain = Enum.map(fills, &Map.put(&1, :chain_type, chain_type)) - {:ok, _} = - Chain.import(%{ - signet_fills: %{params: fills_with_chain}, - timeout: :infinity - }) - - :ok + case Chain.import(%{signet_fills: %{params: fills_with_chain}, timeout: :infinity}) do + {:ok, _} -> :ok + {:error, step, reason, _} -> {:error, {step, reason}} + end end - end diff --git a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs new file mode 100644 index 000000000000..f83b6dc12803 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs @@ -0,0 +1,539 @@ +defmodule Indexer.Fetcher.Signet.EventParserTest do + @moduledoc """ + Table-driven tests for ABI decoding in Indexer.Fetcher.Signet.EventParser. + + Test vectors are derived from the @signet-sh/sdk TypeScript test suite. + ABI encoding helpers construct raw event log data independently of the parser, + providing cross-validation of the manual binary decoder. + """ + + use ExUnit.Case, async: true + + import Bitwise + + alias Indexer.Fetcher.Signet.{Abi, EventParser} + + # -- ABI encoding helpers -- + + defp encode_uint256(value), do: <> + defp encode_uint32(value), do: <<0::224, value::unsigned-big-integer-size(32)>> + + defp encode_address(addr) when byte_size(addr) == 20, do: <<0::96, addr::binary>> + + defp encode_address("0x" <> hex) do + <<0::96, Base.decode16!(hex, case: :mixed)::binary>> + end + + defp encode_input_array(inputs) do + count = encode_uint256(length(inputs)) + + elements = + Enum.map(inputs, fn {token, amount} -> + encode_address(token) <> encode_uint256(amount) + end) + + IO.iodata_to_binary([count | elements]) + end + + defp encode_output_array(outputs) do + count = encode_uint256(length(outputs)) + + elements = + Enum.map(outputs, fn {token, amount, recipient, chain_id} -> + encode_address(token) <> encode_uint256(amount) <> encode_address(recipient) <> encode_uint32(chain_id) + end) + + IO.iodata_to_binary([count | elements]) + end + + defp encode_order_data(deadline, inputs, outputs) do + inputs_encoded = encode_input_array(inputs) + outputs_encoded = encode_output_array(outputs) + + # Offsets are from start of data payload (3 header words = 96 bytes) + inputs_offset = 96 + outputs_offset = inputs_offset + byte_size(inputs_encoded) + + encode_uint256(deadline) <> + encode_uint256(inputs_offset) <> + encode_uint256(outputs_offset) <> + inputs_encoded <> + outputs_encoded + end + + defp encode_filled_data(outputs) do + outputs_encoded = encode_output_array(outputs) + # Single dynamic array: offset word (32) + encoded data + encode_uint256(32) <> outputs_encoded + end + + defp encode_sweep_data(amount), do: encode_uint256(amount) + + defp to_hex(binary), do: "0x" <> Base.encode16(binary, case: :lower) + + defp build_log(opts) do + data = Keyword.fetch!(opts, :data) + topics = Keyword.get(opts, :topics, []) + tx_hash = Keyword.get(opts, :tx_hash, "0x" <> String.duplicate("ab", 32)) + block = Keyword.get(opts, :block, 100) + index = Keyword.get(opts, :index, 0) + + %{ + "data" => to_hex(data), + "topics" => topics, + "transactionHash" => tx_hash, + "blockNumber" => "0x" <> Integer.to_string(block, 16), + "logIndex" => "0x" <> Integer.to_string(index, 16) + } + end + + # -- Addresses used across tests (from @signet-sh/sdk vectors) -- + + @zero_addr "0x0000000000000000000000000000000000000000" + @usdc "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + @usdt "0xdac17f958d2ee523a2206206994597c13d831ec7" + @wbtc "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" + @weth "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + @addr_1234 "0x1234567890123456789012345678901234567890" + @addr_1111 "0x1111111111111111111111111111111111111111" + @addr_2222 "0x2222222222222222222222222222222222222222" + @addr_3333 "0x3333333333333333333333333333333333333333" + @addr_4444 "0x4444444444444444444444444444444444444444" + @addr_6666 "0x6666666666666666666666666666666666666666" + @addr_8888 "0x8888888888888888888888888888888888888888" + @signet_token "0x96f44ddc3bc8892371305531f1a6d8ca2331fe6c" + + # -- Order event decoding tests (from vectors.json) -- + + describe "parse_rollup_logs/1 - Order events" do + @order_vectors [ + %{ + name: "minimal_order", + deadline: 0, + inputs: [{@zero_addr, 0}], + outputs: [{@zero_addr, 0, @zero_addr, 0}], + expected_inputs: [%{"token" => @zero_addr, "amount" => "0"}], + expected_outputs: [%{"token" => @zero_addr, "amount" => "0", "recipient" => @zero_addr, "chainId" => 0}] + }, + %{ + name: "multi_input", + deadline: 0x6553F100, + inputs: [{@usdc, 0xF4240}, {@usdt, 0x1E8480}, {@wbtc, 0x5F5E100}], + outputs: [{@zero_addr, 0xDE0B6B3A7640000, @addr_1234, 1}], + expected_inputs: [ + %{"token" => @usdc, "amount" => "1000000"}, + %{"token" => @usdt, "amount" => "2000000"}, + %{"token" => @wbtc, "amount" => "100000000"} + ], + expected_outputs: [ + %{"token" => @zero_addr, "amount" => "1000000000000000000", "recipient" => @addr_1234, "chainId" => 1} + ] + }, + %{ + name: "multi_output", + deadline: 0x6B49D200, + inputs: [{@usdc, 0x989680}], + outputs: [ + {@usdc, 0x2DC6C0, @addr_1111, 1}, + {@usdc, 0x2DC6C0, @addr_2222, 1}, + {@usdc, 0x3D0900, @addr_3333, 1} + ], + expected_inputs: [%{"token" => @usdc, "amount" => "10000000"}], + expected_outputs: [ + %{"token" => @usdc, "amount" => "3000000", "recipient" => @addr_1111, "chainId" => 1}, + %{"token" => @usdc, "amount" => "3000000", "recipient" => @addr_2222, "chainId" => 1}, + %{"token" => @usdc, "amount" => "4000000", "recipient" => @addr_3333, "chainId" => 1} + ] + }, + %{ + name: "cross_chain", + deadline: 0x684EE180, + inputs: [{@usdc, 0x4C4B40}], + outputs: [ + {@usdc, 0x2625A0, @addr_4444, 1}, + {@usdc, 0x2625A0, @addr_4444, 421_614} + ], + expected_inputs: [%{"token" => @usdc, "amount" => "5000000"}], + expected_outputs: [ + %{"token" => @usdc, "amount" => "2500000", "recipient" => @addr_4444, "chainId" => 1}, + %{"token" => @usdc, "amount" => "2500000", "recipient" => @addr_4444, "chainId" => 421_614} + ] + }, + %{ + name: "large_amounts", + deadline: 0xFFFFFFFFFFFFFFFF, + inputs: [{@weth, 0x21E19E0C9BAB2400000}], + outputs: [{@weth, 0x21E19E0C9BAB2400000, @addr_6666, 1}], + expected_inputs: [%{"token" => @weth, "amount" => "10000000000000000000000"}], + expected_outputs: [ + %{ + "token" => @weth, + "amount" => "10000000000000000000000", + "recipient" => @addr_6666, + "chainId" => 1 + } + ] + }, + %{ + name: "mainnet_config", + deadline: 0x65920080, + inputs: [{@usdc, 0x5F5E100}], + outputs: [ + {@signet_token, 0x2FAF080, @addr_8888, 1}, + {@signet_token, 0x2FAF080, @addr_8888, 519} + ], + expected_inputs: [%{"token" => @usdc, "amount" => "100000000"}], + expected_outputs: [ + %{"token" => @signet_token, "amount" => "50000000", "recipient" => @addr_8888, "chainId" => 1}, + %{"token" => @signet_token, "amount" => "50000000", "recipient" => @addr_8888, "chainId" => 519} + ] + } + ] + + for vector <- @order_vectors do + @vector vector + test "decodes #{@vector.name} Order event" do + v = @vector + data = encode_order_data(v.deadline, v.inputs, v.outputs) + + log = + build_log( + data: data, + topics: [Abi.order_event_topic()], + block: 42, + index: 3, + tx_hash: "0x" <> String.duplicate("01", 32) + ) + + {:ok, {[order], []}} = EventParser.parse_rollup_logs([log]) + + assert order.deadline == v.deadline + assert order.block_number == 42 + assert order.log_index == 3 + assert order.transaction_hash == "0x" <> String.duplicate("01", 32) + assert Jason.decode!(order.inputs_json) == v.expected_inputs + assert Jason.decode!(order.outputs_json) == v.expected_outputs + end + end + end + + # -- Filled event decoding tests -- + + describe "parse_rollup_logs/1 - Filled events" do + @fill_vectors [ + %{ + name: "minimal_fill", + outputs: [{@zero_addr, 0, @zero_addr, 0}], + expected: [%{"token" => @zero_addr, "amount" => "0", "recipient" => @zero_addr, "chainId" => 0}] + }, + %{ + name: "single_weth", + outputs: [{@weth, 1_000_000_000_000_000_000, @addr_1234, 1}], + expected: [ + %{"token" => @weth, "amount" => "1000000000000000000", "recipient" => @addr_1234, "chainId" => 1} + ] + }, + %{ + name: "multi_output", + outputs: [ + {@weth, 500_000_000_000_000_000, @addr_1111, 1}, + {@usdc, 1_000_000_000, @addr_2222, 1} + ], + expected: [ + %{"token" => @weth, "amount" => "500000000000000000", "recipient" => @addr_1111, "chainId" => 1}, + %{"token" => @usdc, "amount" => "1000000000", "recipient" => @addr_2222, "chainId" => 1} + ] + }, + %{ + name: "cross_chain", + outputs: [ + {@usdc, 500_000_000, @addr_4444, 1}, + {@usdc, 500_000_000, @addr_4444, 519} + ], + expected: [ + %{"token" => @usdc, "amount" => "500000000", "recipient" => @addr_4444, "chainId" => 1}, + %{"token" => @usdc, "amount" => "500000000", "recipient" => @addr_4444, "chainId" => 519} + ] + } + ] + + for vector <- @fill_vectors do + @vector vector + test "decodes #{@vector.name} Filled event" do + v = @vector + data = encode_filled_data(v.outputs) + + log = + build_log( + data: data, + topics: [Abi.filled_event_topic()], + block: 99, + index: 7 + ) + + {:ok, {[], [fill]}} = EventParser.parse_rollup_logs([log]) + + assert fill.block_number == 99 + assert fill.log_index == 7 + assert Jason.decode!(fill.outputs_json) == v.expected + end + end + end + + # -- Sweep event decoding tests -- + + describe "parse_rollup_logs/1 - Sweep events" do + test "decodes minimal Sweep event" do + data = encode_sweep_data(0) + zero_topic = "0x" <> String.duplicate("00", 32) + + log = + build_log( + data: data, + topics: [Abi.sweep_event_topic(), zero_topic, zero_topic], + block: 10, + index: 0 + ) + + # Sweep events are only returned as part of order association, not standalone. + # With no orders, sweeps are consumed internally but result in empty orders list. + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + end + + # -- Order + Sweep association tests -- + + describe "parse_rollup_logs/1 - Order + Sweep association" do + test "order gets sweep fields when sweep exists in same tx" do + tx_hash = "0x" <> String.duplicate("aa", 32) + order_data = encode_order_data(1000, [{@usdc, 100}], [{@usdc, 100, @addr_1111, 1}]) + sweep_data = encode_sweep_data(50) + + recipient_topic = "0x000000000000000000000000" <> String.duplicate("22", 20) + token_topic = "0x000000000000000000000000" <> String.duplicate("33", 20) + + order_log = build_log(data: order_data, topics: [Abi.order_event_topic()], tx_hash: tx_hash, block: 1, index: 0) + + sweep_log = + build_log( + data: sweep_data, + topics: [Abi.sweep_event_topic(), recipient_topic, token_topic], + tx_hash: tx_hash, + block: 1, + index: 1 + ) + + {:ok, {[order], []}} = EventParser.parse_rollup_logs([order_log, sweep_log]) + + assert order.sweep_amount == 50 + assert order.sweep_recipient == Base.decode16!(String.duplicate("22", 20), case: :lower) + assert order.sweep_token == Base.decode16!(String.duplicate("33", 20), case: :lower) + end + + test "order has no sweep fields when no sweep in tx" do + order_data = encode_order_data(1000, [{@usdc, 100}], [{@usdc, 100, @addr_1111, 1}]) + log = build_log(data: order_data, topics: [Abi.order_event_topic()], block: 1, index: 0) + + {:ok, {[order], []}} = EventParser.parse_rollup_logs([log]) + + refute Map.has_key?(order, :sweep_recipient) + refute Map.has_key?(order, :sweep_token) + refute Map.has_key?(order, :sweep_amount) + end + + test "warns when multiple sweeps exist for same tx" do + tx_hash = "0x" <> String.duplicate("cc", 32) + order_data = encode_order_data(1000, [{@usdc, 100}], [{@usdc, 100, @addr_1111, 1}]) + + recipient1 = "0x000000000000000000000000" <> String.duplicate("11", 20) + recipient2 = "0x000000000000000000000000" <> String.duplicate("22", 20) + token_topic = "0x000000000000000000000000" <> String.duplicate("ff", 20) + + order_log = build_log(data: order_data, topics: [Abi.order_event_topic()], tx_hash: tx_hash, block: 1, index: 0) + + sweep_log1 = + build_log( + data: encode_sweep_data(10), + topics: [Abi.sweep_event_topic(), recipient1, token_topic], + tx_hash: tx_hash, + block: 1, + index: 1 + ) + + sweep_log2 = + build_log( + data: encode_sweep_data(20), + topics: [Abi.sweep_event_topic(), recipient2, token_topic], + tx_hash: tx_hash, + block: 1, + index: 2 + ) + + # Should still succeed (uses first element from reversed accumulator, i.e. last encountered) + {:ok, {[order], []}} = EventParser.parse_rollup_logs([order_log, sweep_log1, sweep_log2]) + + assert order.sweep_amount in [10, 20] + end + end + + # -- Host filled logs tests -- + + describe "parse_host_filled_logs/1" do + test "parses only Filled events, ignores others" do + fill_data = encode_filled_data([{@usdc, 100, @addr_1111, 1}]) + fill_log = build_log(data: fill_data, topics: [Abi.filled_event_topic()], block: 50, index: 0) + noise_log = build_log(data: <<0::256>>, topics: ["0xdeadbeef" <> String.duplicate("00", 28)], block: 50, index: 1) + + {:ok, fills} = EventParser.parse_host_filled_logs([fill_log, noise_log]) + + assert length(fills) == 1 + assert hd(fills).block_number == 50 + end + + test "returns empty list for empty input" do + {:ok, []} = EventParser.parse_host_filled_logs([]) + end + + test "parses multiple Filled events" do + fill1 = + build_log( + data: encode_filled_data([{@usdc, 100, @addr_1111, 1}]), + topics: [Abi.filled_event_topic()], + block: 10, + index: 0, + tx_hash: "0x" <> String.duplicate("01", 32) + ) + + fill2 = + build_log( + data: encode_filled_data([{@weth, 200, @addr_2222, 519}]), + topics: [Abi.filled_event_topic()], + block: 11, + index: 0, + tx_hash: "0x" <> String.duplicate("02", 32) + ) + + {:ok, fills} = EventParser.parse_host_filled_logs([fill1, fill2]) + + assert length(fills) == 2 + assert Enum.at(fills, 0).block_number == 10 + assert Enum.at(fills, 1).block_number == 11 + end + end + + # -- Edge cases -- + + describe "edge cases" do + test "empty logs returns empty results" do + assert {:ok, {[], []}} = EventParser.parse_rollup_logs([]) + end + + test "logs with unrecognized topics are skipped" do + log = build_log(data: <<0::256>>, topics: ["0x" <> String.duplicate("ff", 32)]) + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + + test "max uint32 chainId (4294967295)" do + max_u32 = 0xFFFFFFFF + + data = encode_order_data(1000, [{@usdc, 100}], [{@usdc, 100, @addr_1111, max_u32}]) + log = build_log(data: data, topics: [Abi.order_event_topic()], block: 1, index: 0) + + {:ok, {[order], []}} = EventParser.parse_rollup_logs([log]) + + [output] = Jason.decode!(order.outputs_json) + assert output["chainId"] == max_u32 + end + + test "max uint256 amount" do + max_u256 = (1 <<< 256) - 1 + + data = encode_filled_data([{@weth, max_u256, @addr_1111, 1}]) + log = build_log(data: data, topics: [Abi.filled_event_topic()], block: 1, index: 0) + + {:ok, {[], [fill]}} = EventParser.parse_rollup_logs([log]) + + [output] = Jason.decode!(fill.outputs_json) + assert output["amount"] == Integer.to_string(max_u256) + end + + test "handles atom-keyed logs" do + data = encode_order_data(500, [{@usdc, 100}], [{@usdc, 100, @addr_1111, 1}]) + + log = %{ + data: "0x" <> Base.encode16(data, case: :lower), + topics: [Abi.order_event_topic()], + transaction_hash: "0x" <> String.duplicate("dd", 32), + block_number: 77, + log_index: 2 + } + + {:ok, {[order], []}} = EventParser.parse_rollup_logs([log]) + + assert order.deadline == 500 + assert order.block_number == 77 + assert order.log_index == 2 + end + + test "handles integer block_number and log_index" do + data = encode_filled_data([{@usdc, 100, @addr_1111, 1}]) + + log = %{ + data: "0x" <> Base.encode16(data, case: :lower), + topics: [Abi.filled_event_topic()], + transaction_hash: "0x" <> String.duplicate("ee", 32), + block_number: 42, + log_index: 5 + } + + {:ok, {[], [fill]}} = EventParser.parse_rollup_logs([log]) + + assert fill.block_number == 42 + assert fill.log_index == 5 + end + end + + # -- Error path tests -- + + describe "error paths" do + test "invalid block_number causes event to be skipped" do + data = encode_order_data(1000, [{@usdc, 100}], [{@usdc, 100, @addr_1111, 1}]) + + log = %{ + "data" => "0x" <> Base.encode16(data, case: :lower), + "topics" => [Abi.order_event_topic()], + "transactionHash" => "0x" <> String.duplicate("ab", 32), + "blockNumber" => nil, + "logIndex" => "0x0" + } + + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + + test "invalid log_index causes event to be skipped" do + data = encode_order_data(1000, [{@usdc, 100}], [{@usdc, 100, @addr_1111, 1}]) + + log = %{ + "data" => "0x" <> Base.encode16(data, case: :lower), + "topics" => [Abi.order_event_topic()], + "transactionHash" => "0x" <> String.duplicate("ab", 32), + "blockNumber" => "0x1", + "logIndex" => nil + } + + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + + test "malformed data causes event to be skipped" do + log = build_log(data: <<1, 2, 3>>, topics: [Abi.order_event_topic()], block: 1, index: 0) + + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + + test "empty data causes Filled event to be skipped" do + log = build_log(data: <<>>, topics: [Abi.filled_event_topic()], block: 1, index: 0) + + {:ok, {[], []}} = EventParser.parse_rollup_logs([log]) + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs index ca7792a37449..57bfcdcd79b8 100644 --- a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -16,10 +16,16 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do import Explorer.Factory alias Explorer.Chain + alias Explorer.Chain.Hash alias Indexer.Fetcher.Signet.OrdersFetcher @moduletag :signet + defp cast_hash!(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + hash + end + describe "OrdersFetcher configuration" do test "child_spec returns proper supervisor config" do json_rpc_named_arguments = [ @@ -46,7 +52,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do describe "database import via Chain.import/1" do test "imports order through Chain.import" do - tx_hash = <<1::256>> + tx_hash = cast_hash!(<<1::256>>) order_params = %{ deadline: 1_700_000_000, @@ -69,7 +75,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do end test "imports fill through Chain.import" do - tx_hash = <<2::256>> + tx_hash = cast_hash!(<<2::256>>) fill_params = %{ chain_type: :rollup, @@ -94,7 +100,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do order_params = %{ deadline: 1_700_000_000, block_number: 100, - transaction_hash: <<10::256>>, + transaction_hash: cast_hash!(<<10::256>>), log_index: 0, inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), outputs_json: @@ -104,7 +110,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do fill_params = %{ chain_type: :host, block_number: 200, - transaction_hash: <<20::256>>, + transaction_hash: cast_hash!(<<20::256>>), log_index: 0, outputs_json: Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) diff --git a/config/config_helper.exs b/config/config_helper.exs index 2d1fbd9c3238..126e0d1f7f1d 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -24,6 +24,7 @@ defmodule ConfigHelper do {:zilliqa, nil} => [Explorer.Repo.Zilliqa], {:zksync, nil} => [Explorer.Repo.ZkSync], {:neon, nil} => [Explorer.Repo.Neon], + {:signet, nil} => [Explorer.Repo.Signet], {:optimism, :celo} => [ Explorer.Repo.Optimism, Explorer.Repo.Celo @@ -424,7 +425,8 @@ defmodule ConfigHelper do "zilliqa" => :zilliqa, "zksync" => :zksync, "neon" => :neon, - "optimism-celo" => {:optimism, :celo} + "optimism-celo" => {:optimism, :celo}, + "signet" => :signet } @doc """ diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index f40189052ac2..5b8ff87e0d41 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -142,7 +142,8 @@ for repo <- [ # Feature dependent repos Explorer.Repo.BridgedTokens, Explorer.Repo.ShrunkInternalTransactions, - Explorer.Repo.Neon + Explorer.Repo.Neon, + Explorer.Repo.Signet ] do config :explorer, repo, database: database, diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index d00c8187744e..e7c303a57fe6 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -104,7 +104,8 @@ for repo <- [ Explorer.Repo.Stability, Explorer.Repo.Zilliqa, Explorer.Repo.ZkSync, - Explorer.Repo.Neon + Explorer.Repo.Neon, + Explorer.Repo.Signet ] do config :explorer, repo, url: ConfigHelper.parse_url_env_var("DATABASE_URL"), diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 000000000000..a19af11ee31b --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,17 @@ +services: + db: + image: postgres:17 + command: postgres -c 'max_connections=250' + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: explorer_test + ports: + - "5432:5432" + tmpfs: + - /var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 2s + timeout: 5s + retries: 10 From 821c6acbeba7182963d1b7631489f1d0e4a53725 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Fri, 27 Feb 2026 12:06:33 +0000 Subject: [PATCH 14/24] fix: wrap Signet tests in chain_type check and add to CI matrix - Wrap orders_test.exs, fills_test.exs, and orders_fetcher_test.exs in compile-time chain_type check (if Application.compile_env == :signet) - Add 'signet' to CI matrix chain types - Add Explorer.Repo.Signet to test_helper.exs Sandbox mode This ensures Signet database tests only run when CHAIN_TYPE=signet, preventing 'relation does not exist' errors when running tests with default chain type. --- .github/workflows/config.yml | 1 + .../chain/import/runner/signet/fills_test.exs | 408 +++++++++--------- .../import/runner/signet/orders_test.exs | 344 +++++++-------- apps/explorer/test/test_helper.exs | 1 + .../fetcher/signet/orders_fetcher_test.exs | 304 ++++++------- 5 files changed, 533 insertions(+), 525 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index baac41c80403..8b488af903e4 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -62,6 +62,7 @@ jobs: "rsk", "scroll", "shibarium", + "signet", "stability", "zetachain", "zilliqa", diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs index 1831c869a20f..a90e535a231e 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -1,231 +1,233 @@ -defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do - use Explorer.DataCase +if Application.compile_env(:explorer, :chain_type) == :signet do + defmodule Explorer.Chain.Import.Runner.Signet.FillsTest do + use Explorer.DataCase - alias Ecto.Multi - alias Explorer.Chain.Hash - alias Explorer.Chain.Import.Runner.Signet.Fills, as: FillsRunner - alias Explorer.Chain.Signet.Fill - alias Explorer.Repo + alias Ecto.Multi + alias Explorer.Chain.Hash + alias Explorer.Chain.Import.Runner.Signet.Fills, as: FillsRunner + alias Explorer.Chain.Signet.Fill + alias Explorer.Repo - @moduletag :signet + @moduletag :signet - defp cast_hash!(bytes) do - {:ok, hash} = Hash.Full.cast(bytes) - hash - end - - describe "run/3" do - test "inserts a new rollup fill" do - tx_hash = cast_hash!(<<1::256>>) - - params = [ - %{ - chain_type: :rollup, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi = - Multi.new() - |> FillsRunner.run(params, %{timestamps: timestamps}) - - assert {:ok, %{insert_signet_fills: [fill]}} = Repo.transaction(multi) - assert fill.block_number == 100 - assert fill.chain_type == :rollup - assert fill.log_index == 0 + defp cast_hash!(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + hash end - test "inserts a new host fill" do - tx_hash = cast_hash!(<<2::256>>) + describe "run/3" do + test "inserts a new rollup fill" do + tx_hash = cast_hash!(<<1::256>>) - params = [ - %{ - chain_type: :host, - block_number: 200, - transaction_hash: tx_hash, - log_index: 1, - outputs_json: - Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) - } - ] + params = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + ] - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) - {:ok, %{insert_signet_fills: [fill]}} = Repo.transaction(multi) + multi = + Multi.new() + |> FillsRunner.run(params, %{timestamps: timestamps}) - assert fill.block_number == 200 - assert fill.chain_type == :host - end + assert {:ok, %{insert_signet_fills: [fill]}} = Repo.transaction(multi) + assert fill.block_number == 100 + assert fill.chain_type == :rollup + assert fill.log_index == 0 + end - test "same transaction can have fills on different chains" do - tx_hash = cast_hash!(<<3::256>>) - - rollup_params = [ - %{ - chain_type: :rollup, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) - } - ] - - host_params = [ - %{ - chain_type: :host, - block_number: 200, - transaction_hash: tx_hash, - # Same log_index but different chain_type - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi1 = Multi.new() |> FillsRunner.run(rollup_params, %{timestamps: timestamps}) - {:ok, _} = Repo.transaction(multi1) - - multi2 = Multi.new() |> FillsRunner.run(host_params, %{timestamps: timestamps}) - {:ok, _} = Repo.transaction(multi2) - - # Should have two fills (one per chain type) - assert Repo.aggregate(Fill, :count) == 2 - - # Verify both exist - rollup_fill = Repo.get_by(Fill, chain_type: :rollup, transaction_hash: tx_hash, log_index: 0) - host_fill = Repo.get_by(Fill, chain_type: :host, transaction_hash: tx_hash, log_index: 0) - - assert rollup_fill.block_number == 100 - assert host_fill.block_number == 200 - end + test "inserts a new host fill" do + tx_hash = cast_hash!(<<2::256>>) - test "handles duplicate fills with upsert on composite primary key" do - tx_hash = cast_hash!(<<4::256>>) - - params1 = [ - %{ - chain_type: :rollup, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}]) - } - ] - - params2 = [ - %{ - chain_type: :rollup, - # Different block - block_number: 101, - transaction_hash: tx_hash, - # Same log_index + chain_type - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi1 = Multi.new() |> FillsRunner.run(params1, %{timestamps: timestamps}) - {:ok, _} = Repo.transaction(multi1) - - multi2 = Multi.new() |> FillsRunner.run(params2, %{timestamps: timestamps}) - {:ok, _} = Repo.transaction(multi2) - - # Should only have one fill for this chain_type + tx_hash + log_index combo - assert Repo.aggregate(Fill, :count) == 1 - - fill = Repo.one!(Fill) - # Updated - assert fill.block_number == 101 - end + params = [ + %{ + chain_type: :host, + block_number: 200, + transaction_hash: tx_hash, + log_index: 1, + outputs_json: + Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + } + ] - test "different log_index creates separate fills" do - tx_hash = cast_hash!(<<5::256>>) - - params = [ - %{ - chain_type: :rollup, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}]) - }, - %{ - chain_type: :rollup, - block_number: 100, - transaction_hash: tx_hash, - # Different log_index - log_index: 1, - outputs_json: - Jason.encode!([%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) - {:ok, %{insert_signet_fills: fills}} = Repo.transaction(multi) - - assert length(fills) == 2 - assert Repo.aggregate(Fill, :count) == 2 - end + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - test "inserts multiple fills in batch" do - params = - for i <- 1..5 do + multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_fills: [fill]}} = Repo.transaction(multi) + + assert fill.block_number == 200 + assert fill.chain_type == :host + end + + test "same transaction can have fills on different chains" do + tx_hash = cast_hash!(<<3::256>>) + + rollup_params = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + } + ] + + host_params = [ %{ - chain_type: if(rem(i, 2) == 0, do: :host, else: :rollup), - block_number: 100 + i, - transaction_hash: cast_hash!(<<100 + i::256>>), + chain_type: :host, + block_number: 200, + transaction_hash: tx_hash, + # Same log_index but different chain_type log_index: 0, outputs_json: - Jason.encode!([ - %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} - ]) + Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) } - end + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi1 = Multi.new() |> FillsRunner.run(rollup_params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi1) + + multi2 = Multi.new() |> FillsRunner.run(host_params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi2) + + # Should have two fills (one per chain type) + assert Repo.aggregate(Fill, :count) == 2 - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + # Verify both exist + rollup_fill = Repo.get_by(Fill, chain_type: :rollup, transaction_hash: tx_hash, log_index: 0) + host_fill = Repo.get_by(Fill, chain_type: :host, transaction_hash: tx_hash, log_index: 0) - multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) - {:ok, %{insert_signet_fills: fills}} = Repo.transaction(multi) + assert rollup_fill.block_number == 100 + assert host_fill.block_number == 200 + end - assert length(fills) == 5 - assert Repo.aggregate(Fill, :count) == 5 + test "handles duplicate fills with upsert on composite primary key" do + tx_hash = cast_hash!(<<4::256>>) - # Verify chain type distribution - rollup_count = Enum.count(fills, &(&1.chain_type == :rollup)) - host_count = Enum.count(fills, &(&1.chain_type == :host)) - # i = 1, 3, 5 - assert rollup_count == 3 - # i = 2, 4 - assert host_count == 2 + params1 = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}]) + } + ] + + params2 = [ + %{ + chain_type: :rollup, + # Different block + block_number: 101, + transaction_hash: tx_hash, + # Same log_index + chain_type + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi1 = Multi.new() |> FillsRunner.run(params1, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi1) + + multi2 = Multi.new() |> FillsRunner.run(params2, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi2) + + # Should only have one fill for this chain_type + tx_hash + log_index combo + assert Repo.aggregate(Fill, :count) == 1 + + fill = Repo.one!(Fill) + # Updated + assert fill.block_number == 101 + end + + test "different log_index creates separate fills" do + tx_hash = cast_hash!(<<5::256>>) + + params = [ + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}]) + }, + %{ + chain_type: :rollup, + block_number: 100, + transaction_hash: tx_hash, + # Different log_index + log_index: 1, + outputs_json: + Jason.encode!([%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_fills: fills}} = Repo.transaction(multi) + + assert length(fills) == 2 + assert Repo.aggregate(Fill, :count) == 2 + end + + test "inserts multiple fills in batch" do + params = + for i <- 1..5 do + %{ + chain_type: if(rem(i, 2) == 0, do: :host, else: :rollup), + block_number: 100 + i, + transaction_hash: cast_hash!(<<100 + i::256>>), + log_index: 0, + outputs_json: + Jason.encode!([ + %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} + ]) + } + end + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> FillsRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_fills: fills}} = Repo.transaction(multi) + + assert length(fills) == 5 + assert Repo.aggregate(Fill, :count) == 5 + + # Verify chain type distribution + rollup_count = Enum.count(fills, &(&1.chain_type == :rollup)) + host_count = Enum.count(fills, &(&1.chain_type == :host)) + # i = 1, 3, 5 + assert rollup_count == 3 + # i = 2, 4 + assert host_count == 2 + end end - end - describe "ecto_schema_module/0" do - test "returns Fill module" do - assert FillsRunner.ecto_schema_module() == Fill + describe "ecto_schema_module/0" do + test "returns Fill module" do + assert FillsRunner.ecto_schema_module() == Fill + end end - end - describe "option_key/0" do - test "returns :signet_fills" do - assert FillsRunner.option_key() == :signet_fills + describe "option_key/0" do + test "returns :signet_fills" do + assert FillsRunner.option_key() == :signet_fills + end end end end diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs index dc0ab5a4acae..5cf160c5fb11 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -1,193 +1,195 @@ -defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do - use Explorer.DataCase +if Application.compile_env(:explorer, :chain_type) == :signet do + defmodule Explorer.Chain.Import.Runner.Signet.OrdersTest do + use Explorer.DataCase - alias Ecto.Multi - alias Explorer.Chain.Hash - alias Explorer.Chain.Import.Runner.Signet.Orders, as: OrdersRunner - alias Explorer.Chain.Signet.Order - alias Explorer.Repo + alias Ecto.Multi + alias Explorer.Chain.Hash + alias Explorer.Chain.Import.Runner.Signet.Orders, as: OrdersRunner + alias Explorer.Chain.Signet.Order + alias Explorer.Repo - @moduletag :signet + @moduletag :signet - defp cast_hash!(bytes) do - {:ok, hash} = Hash.Full.cast(bytes) - hash - end - - describe "run/3" do - test "inserts a new order" do - tx_hash = cast_hash!(<<1::256>>) - - params = [ - %{ - deadline: 1_700_000_000, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi = - Multi.new() - |> OrdersRunner.run(params, %{timestamps: timestamps}) - - assert {:ok, %{insert_signet_orders: [order]}} = Repo.transaction(multi) - assert order.block_number == 100 - assert order.deadline == 1_700_000_000 - assert order.log_index == 0 + defp cast_hash!(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + hash end - test "handles duplicate orders with upsert on composite primary key" do - tx_hash = cast_hash!(<<2::256>>) - - params = [ - %{ - deadline: 1_700_000_000, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - # Insert first time - multi1 = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) - {:ok, _} = Repo.transaction(multi1) - - # Insert second time with same tx_hash + log_index but different data - updated_params = [ - %{ - # Different deadline - deadline: 1_700_000_001, - # Different block - block_number: 101, - transaction_hash: tx_hash, - # Same log_index - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "2000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}]) - } - ] - - multi2 = Multi.new() |> OrdersRunner.run(updated_params, %{timestamps: timestamps}) - {:ok, _} = Repo.transaction(multi2) - - # Should only have one order - assert Repo.aggregate(Order, :count) == 1 - - # Order should be updated - order = Repo.get_by(Order, transaction_hash: tx_hash, log_index: 0) - assert order.deadline == 1_700_000_001 - assert order.block_number == 101 - end + describe "run/3" do + test "inserts a new order" do + tx_hash = cast_hash!(<<1::256>>) - test "different log_index creates separate orders" do - tx_hash = cast_hash!(<<3::256>>) - - params = [ - %{ - deadline: 1_700_000_000, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) - }, - %{ - deadline: 1_700_000_001, - block_number: 100, - transaction_hash: tx_hash, - # Different log_index - log_index: 1, - inputs_json: Jason.encode!([%{"token" => "0x4444", "amount" => "2000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}]) - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) - {:ok, %{insert_signet_orders: orders}} = Repo.transaction(multi) - - assert length(orders) == 2 - assert Repo.aggregate(Order, :count) == 2 - end + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + ] - test "inserts order with sweep data" do - tx_hash = cast_hash!(<<4::256>>) - {:ok, sweep_recipient} = Explorer.Chain.Hash.Address.cast(<<5::160>>) - {:ok, sweep_token} = Explorer.Chain.Hash.Address.cast(<<6::160>>) - - params = [ - %{ - deadline: 1_700_000_000, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), - sweep_recipient: sweep_recipient, - sweep_token: sweep_token, - sweep_amount: %Explorer.Chain.Wei{value: Decimal.new("12345")} - } - ] - - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} - - multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) - {:ok, %{insert_signet_orders: [order]}} = Repo.transaction(multi) - - assert order.sweep_amount == %Explorer.Chain.Wei{value: Decimal.new("12345")} - end + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = + Multi.new() + |> OrdersRunner.run(params, %{timestamps: timestamps}) + + assert {:ok, %{insert_signet_orders: [order]}} = Repo.transaction(multi) + assert order.block_number == 100 + assert order.deadline == 1_700_000_000 + assert order.log_index == 0 + end + + test "handles duplicate orders with upsert on composite primary key" do + tx_hash = cast_hash!(<<2::256>>) + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + # Insert first time + multi1 = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi1) - test "inserts multiple orders in batch" do - params = - for i <- 1..5 do + # Insert second time with same tx_hash + log_index but different data + updated_params = [ %{ - deadline: 1_700_000_000 + i, - block_number: 100 + i, - transaction_hash: cast_hash!(<<100 + i::256>>), + # Different deadline + deadline: 1_700_000_001, + # Different block + block_number: 101, + transaction_hash: tx_hash, + # Same log_index log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}]), + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "2000"}]), outputs_json: - Jason.encode!([ - %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} - ]) + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}]) } - end + ] - timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + multi2 = Multi.new() |> OrdersRunner.run(updated_params, %{timestamps: timestamps}) + {:ok, _} = Repo.transaction(multi2) - multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) - {:ok, %{insert_signet_orders: orders}} = Repo.transaction(multi) + # Should only have one order + assert Repo.aggregate(Order, :count) == 1 - assert length(orders) == 5 - assert Repo.aggregate(Order, :count) == 5 + # Order should be updated + order = Repo.get_by(Order, transaction_hash: tx_hash, log_index: 0) + assert order.deadline == 1_700_000_001 + assert order.block_number == 101 + end + + test "different log_index creates separate orders" do + tx_hash = cast_hash!(<<3::256>>) + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + }, + %{ + deadline: 1_700_000_001, + block_number: 100, + transaction_hash: tx_hash, + # Different log_index + log_index: 1, + inputs_json: Jason.encode!([%{"token" => "0x4444", "amount" => "2000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}]) + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_orders: orders}} = Repo.transaction(multi) + + assert length(orders) == 2 + assert Repo.aggregate(Order, :count) == 2 + end + + test "inserts order with sweep data" do + tx_hash = cast_hash!(<<4::256>>) + {:ok, sweep_recipient} = Explorer.Chain.Hash.Address.cast(<<5::160>>) + {:ok, sweep_token} = Explorer.Chain.Hash.Address.cast(<<6::160>>) + + params = [ + %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), + sweep_recipient: sweep_recipient, + sweep_token: sweep_token, + sweep_amount: %Explorer.Chain.Wei{value: Decimal.new("12345")} + } + ] + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_orders: [order]}} = Repo.transaction(multi) + + assert order.sweep_amount == %Explorer.Chain.Wei{value: Decimal.new("12345")} + end + + test "inserts multiple orders in batch" do + params = + for i <- 1..5 do + %{ + deadline: 1_700_000_000 + i, + block_number: 100 + i, + transaction_hash: cast_hash!(<<100 + i::256>>), + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}]), + outputs_json: + Jason.encode!([ + %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} + ]) + } + end + + timestamps = %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + + multi = Multi.new() |> OrdersRunner.run(params, %{timestamps: timestamps}) + {:ok, %{insert_signet_orders: orders}} = Repo.transaction(multi) + + assert length(orders) == 5 + assert Repo.aggregate(Order, :count) == 5 + end end - end - describe "ecto_schema_module/0" do - test "returns Order module" do - assert OrdersRunner.ecto_schema_module() == Order + describe "ecto_schema_module/0" do + test "returns Order module" do + assert OrdersRunner.ecto_schema_module() == Order + end end - end - describe "option_key/0" do - test "returns :signet_orders" do - assert OrdersRunner.option_key() == :signet_orders + describe "option_key/0" do + test "returns :signet_orders" do + assert OrdersRunner.option_key() == :signet_orders + end end end end diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index 55deb8e9949c..8fe047dd5a7e 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -27,6 +27,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Mud, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.ShrunkInternalTransactions, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Signet, :auto) Mox.defmock(Explorer.Market.Source.TestSource, for: Explorer.Market.Source) Mox.defmock(Explorer.History.TestHistorian, for: Explorer.History.Historian) diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs index 57bfcdcd79b8..28f68e1007d0 100644 --- a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -1,167 +1,169 @@ -defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do - @moduledoc """ - Integration tests for the Signet OrdersFetcher module. +if Application.compile_env(:explorer, :chain_type) == :signet do + defmodule Indexer.Fetcher.Signet.OrdersFetcherTest do + @moduledoc """ + Integration tests for the Signet OrdersFetcher module. - Tests verify the full pipeline from event fetching through - database insertion. + Tests verify the full pipeline from event fetching through + database insertion. - Note: Orders and fills are indexed independently with no correlation. - Primary keys are: - - Orders: (transaction_hash, log_index) - - Fills: (chain_type, transaction_hash, log_index) - """ + Note: Orders and fills are indexed independently with no correlation. + Primary keys are: + - Orders: (transaction_hash, log_index) + - Fills: (chain_type, transaction_hash, log_index) + """ - use Explorer.DataCase, async: false + use Explorer.DataCase, async: false - import Explorer.Factory + import Explorer.Factory - alias Explorer.Chain - alias Explorer.Chain.Hash - alias Indexer.Fetcher.Signet.OrdersFetcher + alias Explorer.Chain + alias Explorer.Chain.Hash + alias Indexer.Fetcher.Signet.OrdersFetcher - @moduletag :signet + @moduletag :signet - defp cast_hash!(bytes) do - {:ok, hash} = Hash.Full.cast(bytes) - hash - end - - describe "OrdersFetcher configuration" do - test "child_spec returns proper supervisor config" do - json_rpc_named_arguments = [ - transport: EthereumJSONRPC.Mox, - transport_options: [] - ] - - Application.put_env(:indexer, OrdersFetcher, - enabled: true, - rollup_orders_address: "0x1234567890123456789012345678901234567890", - recheck_interval: 1000 - ) - - child_spec = - OrdersFetcher.child_spec([ - [json_rpc_named_arguments: json_rpc_named_arguments], - [name: OrdersFetcher] - ]) - - assert child_spec.id == OrdersFetcher - assert child_spec.restart == :transient - end - end - - describe "database import via Chain.import/1" do - test "imports order through Chain.import" do - tx_hash = cast_hash!(<<1::256>>) - - order_params = %{ - deadline: 1_700_000_000, - block_number: 100, - transaction_hash: tx_hash, - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) - } - - assert {:ok, %{insert_signet_orders: [order]}} = - Chain.import(%{ - signet_orders: %{params: [order_params]}, - timeout: :infinity - }) - - assert order.block_number == 100 - assert order.deadline == 1_700_000_000 - end - - test "imports fill through Chain.import" do - tx_hash = cast_hash!(<<2::256>>) - - fill_params = %{ - chain_type: :rollup, - block_number: 150, - transaction_hash: tx_hash, - log_index: 1, - outputs_json: - Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) - } - - assert {:ok, %{insert_signet_fills: [fill]}} = - Chain.import(%{ - signet_fills: %{params: [fill_params]}, - timeout: :infinity - }) - - assert fill.block_number == 150 - assert fill.chain_type == :rollup - end - - test "imports order and fill together" do - order_params = %{ - deadline: 1_700_000_000, - block_number: 100, - transaction_hash: cast_hash!(<<10::256>>), - log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), - outputs_json: - Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) - } - - fill_params = %{ - chain_type: :host, - block_number: 200, - transaction_hash: cast_hash!(<<20::256>>), - log_index: 0, - outputs_json: - Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) - } - - assert {:ok, result} = - Chain.import(%{ - signet_orders: %{params: [order_params]}, - signet_fills: %{params: [fill_params]}, - timeout: :infinity - }) - - assert length(result.insert_signet_orders) == 1 - assert length(result.insert_signet_fills) == 1 - end - end - - describe "factory integration" do - test "signet_order factory creates valid order" do - order = insert(:signet_order) - - assert order.transaction_hash != nil - assert order.log_index != nil - assert order.deadline != nil - assert order.block_number != nil - assert order.inputs_json != nil - assert order.outputs_json != nil + defp cast_hash!(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + hash end - test "signet_fill factory creates valid fill" do - fill = insert(:signet_fill) - - assert fill.transaction_hash != nil - assert fill.log_index != nil - assert fill.chain_type in [:rollup, :host] - assert fill.block_number != nil - assert fill.outputs_json != nil + describe "OrdersFetcher configuration" do + test "child_spec returns proper supervisor config" do + json_rpc_named_arguments = [ + transport: EthereumJSONRPC.Mox, + transport_options: [] + ] + + Application.put_env(:indexer, OrdersFetcher, + enabled: true, + rollup_orders_address: "0x1234567890123456789012345678901234567890", + recheck_interval: 1000 + ) + + child_spec = + OrdersFetcher.child_spec([ + [json_rpc_named_arguments: json_rpc_named_arguments], + [name: OrdersFetcher] + ]) + + assert child_spec.id == OrdersFetcher + assert child_spec.restart == :transient + end end - test "factory orders can be customized" do - order = insert(:signet_order, deadline: 9_999_999_999, block_number: 42) - - assert order.deadline == 9_999_999_999 - assert order.block_number == 42 + describe "database import via Chain.import/1" do + test "imports order through Chain.import" do + tx_hash = cast_hash!(<<1::256>>) + + order_params = %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: tx_hash, + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + } + + assert {:ok, %{insert_signet_orders: [order]}} = + Chain.import(%{ + signet_orders: %{params: [order_params]}, + timeout: :infinity + }) + + assert order.block_number == 100 + assert order.deadline == 1_700_000_000 + end + + test "imports fill through Chain.import" do + tx_hash = cast_hash!(<<2::256>>) + + fill_params = %{ + chain_type: :rollup, + block_number: 150, + transaction_hash: tx_hash, + log_index: 1, + outputs_json: + Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + } + + assert {:ok, %{insert_signet_fills: [fill]}} = + Chain.import(%{ + signet_fills: %{params: [fill_params]}, + timeout: :infinity + }) + + assert fill.block_number == 150 + assert fill.chain_type == :rollup + end + + test "imports order and fill together" do + order_params = %{ + deadline: 1_700_000_000, + block_number: 100, + transaction_hash: cast_hash!(<<10::256>>), + log_index: 0, + inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), + outputs_json: + Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + } + + fill_params = %{ + chain_type: :host, + block_number: 200, + transaction_hash: cast_hash!(<<20::256>>), + log_index: 0, + outputs_json: + Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + } + + assert {:ok, result} = + Chain.import(%{ + signet_orders: %{params: [order_params]}, + signet_fills: %{params: [fill_params]}, + timeout: :infinity + }) + + assert length(result.insert_signet_orders) == 1 + assert length(result.insert_signet_fills) == 1 + end end - test "factory fills can be customized" do - fill = insert(:signet_fill, chain_type: :host, block_number: 123) - - assert fill.chain_type == :host - assert fill.block_number == 123 + describe "factory integration" do + test "signet_order factory creates valid order" do + order = insert(:signet_order) + + assert order.transaction_hash != nil + assert order.log_index != nil + assert order.deadline != nil + assert order.block_number != nil + assert order.inputs_json != nil + assert order.outputs_json != nil + end + + test "signet_fill factory creates valid fill" do + fill = insert(:signet_fill) + + assert fill.transaction_hash != nil + assert fill.log_index != nil + assert fill.chain_type in [:rollup, :host] + assert fill.block_number != nil + assert fill.outputs_json != nil + end + + test "factory orders can be customized" do + order = insert(:signet_order, deadline: 9_999_999_999, block_number: 42) + + assert order.deadline == 9_999_999_999 + assert order.block_number == 42 + end + + test "factory fills can be customized" do + fill = insert(:signet_fill, chain_type: :host, block_number: 123) + + assert fill.chain_type == :host + assert fill.block_number == 123 + end end end end From 0acfe230a7dba12949a4e05e7ee52231e5343220 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Fri, 27 Feb 2026 12:09:21 +0000 Subject: [PATCH 15/24] fix: reduce nesting depth in EventParser to pass Credo Refactor parse_host_filled_logs to use filter + flat_map pattern instead of deeply nested reduce, fixing Credo 'nested too deep' warning. --- .../indexer/fetcher/signet/event_parser.ex | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index 40624c98af07..29b6d25cd62d 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -70,25 +70,23 @@ defmodule Indexer.Fetcher.Signet.EventParser do fills = logs |> Enum.map(&normalize_log/1) - |> Enum.reduce([], fn log, acc -> - if Enum.at(log.topics, 0) == filled_topic do - case parse_filled_event(log) do - {:ok, fill} -> - [fill | acc] - - {:error, reason} -> - Logger.warning("Failed to parse host Filled event: #{inspect(reason)}") - acc - end - else - acc - end - end) - |> Enum.reverse() + |> Enum.filter(&(Enum.at(&1.topics, 0) == filled_topic)) + |> Enum.flat_map(&parse_host_fill_log/1) {:ok, fills} end + defp parse_host_fill_log(log) do + case parse_filled_event(log) do + {:ok, fill} -> + [fill] + + {:error, reason} -> + Logger.warning("Failed to parse host Filled event: #{inspect(reason)}") + [] + end + end + # Normalize log keys from JSON-RPC string keys or Elixir atom keys into a # consistent atom-keyed map. Called once at the entry point so all downstream # functions work with a single format. From b231272c6be612508e5037165449b00e02185a46 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Fri, 27 Feb 2026 13:07:19 +0000 Subject: [PATCH 16/24] fix: address remaining PR review feedback for Signet Orders Indexer - Add else clauses to with chains in fetch_and_process_rollup_events, fetch_and_process_host_events, and fetch_historical_events so errors propagate as {:error, reason} instead of falling through silently - Replace manual Integer.parse hex parsing in get_latest_block with EthereumJSONRPC.quantity_to_integer/1 for robustness - Query both Order and rollup Fill tables in get_last_processed_block for :rollup chain_type and take the max to avoid re-indexing gaps - Parse log_index in sweep events and use nearest-log_index matching when multiple sweeps exist per transaction instead of blindly using the first sweep - Add bounds checking on ABI offsets in decode_order_data to return {:error, :invalid_abi_offsets} instead of crashing on malformed data Co-Authored-By: Claude Opus 4.6 --- .../indexer/fetcher/signet/event_parser.ex | 33 +++++++++------ .../indexer/fetcher/signet/orders_fetcher.ex | 40 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index 29b6d25cd62d..6027dc5ff5c5 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -172,10 +172,12 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp parse_sweep_event(log) do data = decode_hex_data(log.data) - with {:ok, amount} <- decode_sweep_data(data) do + with {:ok, amount} <- decode_sweep_data(data), + {:ok, log_index} <- parse_log_index(log) do {:ok, %{ transaction_hash: format_transaction_hash(log.transaction_hash), + log_index: log_index, recipient: decode_indexed_address(Enum.at(log.topics, 1)), token: decode_indexed_address(Enum.at(log.topics, 2)), amount: amount @@ -189,19 +191,24 @@ defmodule Indexer.Fetcher.Signet.EventParser do <> = data + rest_size = byte_size(rest) + # ABI offsets are from the start of the data payload; subtract the header # size to get the position within `rest` (which starts after the header). - inputs_data = - binary_part(rest, inputs_offset - @order_header_size, byte_size(rest) - inputs_offset + @order_header_size) - - inputs = decode_input_array(inputs_data) + inputs_rel = inputs_offset - @order_header_size + outputs_rel = outputs_offset - @order_header_size - outputs_data = - binary_part(rest, outputs_offset - @order_header_size, byte_size(rest) - outputs_offset + @order_header_size) + if inputs_rel < 0 or inputs_rel > rest_size or outputs_rel < 0 or outputs_rel > rest_size do + {:error, :invalid_abi_offsets} + else + inputs_data = binary_part(rest, inputs_rel, rest_size - inputs_rel) + inputs = decode_input_array(inputs_data) - outputs = decode_output_array(outputs_data) + outputs_data = binary_part(rest, outputs_rel, rest_size - outputs_rel) + outputs = decode_output_array(outputs_data) - {:ok, {deadline, inputs, outputs}} + {:ok, {deadline, inputs, outputs}} + end rescue e -> Logger.error("Error decoding Order data: #{inspect(e)}") @@ -267,10 +274,10 @@ defmodule Indexer.Fetcher.Signet.EventParser do [sweep] -> attach_sweep(order, sweep) - [sweep | rest] -> - Logger.warning("Multiple sweeps (#{length(rest) + 1}) for tx #{order.transaction_hash}, using first") - - attach_sweep(order, sweep) + [_ | _] = tx_sweeps -> + # Pick the sweep with the closest log_index to the order + nearest = Enum.min_by(tx_sweeps, &abs(&1.log_index - order.log_index)) + attach_sweep(order, nearest) _ -> order diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 7caec013e18d..510521707508 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -306,6 +306,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do updated_task_data = put_in(state.task_data.check_new_rollup.start_block, end_block + 1) {:ok, %{state | task_data: updated_task_data}} + else + {:error, reason} -> {:error, reason} end end @@ -329,6 +331,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do updated_task_data = put_in(state.task_data.check_new_host.start_block, end_block + 1) {:ok, %{state | task_data: updated_task_data}} + else + {:error, reason} -> {:error, reason} end end @@ -356,6 +360,8 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do updated_task_data = put_in(state.task_data.check_historical.end_block, start_block - 1) status = if start_block <= 0, do: :done, else: :continue {:ok, %{state | task_data: updated_task_data}, status} + else + {:error, reason} -> {:error, reason} end end end @@ -392,17 +398,12 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do } case EthereumJSONRPC.json_rpc(request, json_rpc_named_arguments) do - {:ok, hex_block} when is_binary(hex_block) -> - hex = String.trim_leading(hex_block, "0x") - - case Integer.parse(hex, 16) do - {block, ""} -> {:ok, block} - _ -> {:error, {:invalid_block_number, hex_block}} + {:ok, result} -> + case EthereumJSONRPC.quantity_to_integer(result) do + nil -> {:error, {:invalid_block_number, result}} + block -> {:ok, block} end - {:ok, unexpected} -> - {:error, {:unexpected_response, unexpected}} - {:error, reason} -> {:error, reason} end @@ -413,11 +414,22 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do # Falls back to default_start if no records exist case chain_type do :rollup -> - case Explorer.Repo.one( - from(o in Order, - select: max(o.block_number) - ) - ) do + order_max = + Explorer.Repo.one( + from(o in Order, + select: max(o.block_number) + ) + ) + + fill_max = + Explorer.Repo.one( + from(f in Fill, + where: f.chain_type == :rollup, + select: max(f.block_number) + ) + ) + + case max(order_max, fill_max) do nil -> default_start block -> block + 1 end From f429dcee8932a0e784d19faa1e14a7fc75ad05a9 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 18:45:14 +0000 Subject: [PATCH 17/24] fix: complete PR review feedback - factory sequences, JSONB prep - Use sequence for log_index in factories to prevent key collisions - Remove Jason.encode! from factory (prep for JSONB migration) - Additional cleanup from review feedback --- .../lib/explorer/chain/signet/fill.ex | 2 +- .../lib/explorer/chain/signet/order.ex | 12 ++-- .../contracts_abi/signet/host_orders.json | 3 +- .../contracts_abi/signet/rollup_orders.json | 3 +- .../20260216040000_create_signet_tables.exs | 8 +-- apps/explorer/test/support/factory.ex | 45 ++++++------- .../indexer/fetcher/signet/event_parser.ex | 67 +++++++++++++------ .../indexer/fetcher/signet/orders_fetcher.ex | 14 ++-- .../test/indexer/fetcher/signet/abi_test.exs | 41 ++++++++++++ docker-compose.test.yml | 17 ----- 10 files changed, 133 insertions(+), 79 deletions(-) delete mode 100644 docker-compose.test.yml diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex index e56438b1192d..33ea7a41c9eb 100644 --- a/apps/explorer/lib/explorer/chain/signet/fill.ex +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -48,7 +48,7 @@ defmodule Explorer.Chain.Signet.Fill do field(:transaction_hash, Hash.Full, primary_key: true) field(:log_index, :integer, primary_key: true) field(:block_number, :integer) - field(:outputs_json, :string) + field(:outputs_json, :map) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/signet/order.ex b/apps/explorer/lib/explorer/chain/signet/order.ex index 28b43ee55183..ce16ed1a5dd5 100644 --- a/apps/explorer/lib/explorer/chain/signet/order.ex +++ b/apps/explorer/lib/explorer/chain/signet/order.ex @@ -30,8 +30,8 @@ defmodule Explorer.Chain.Signet.Order do * `log_index` - The index of the log within the transaction (primary key) * `deadline` - The deadline timestamp for the order * `block_number` - The block number where the order was created - * `inputs_json` - JSON-encoded array of input tokens and amounts - * `outputs_json` - JSON-encoded array of output tokens, amounts, recipients, and chainIds. + * `inputs_json` - List of input tokens and amounts (stored as JSONB) + * `outputs_json` - List of output tokens, amounts, recipients, and chainIds (stored as JSONB). NOTE: In Order events, the `chainId` field represents the DESTINATION chain (where assets should be delivered), not the chain where the order was created. * `sweep_recipient` - Recipient address from Sweep event (if any) @@ -43,8 +43,8 @@ defmodule Explorer.Chain.Signet.Order do block_number: non_neg_integer(), transaction_hash: binary(), log_index: non_neg_integer(), - inputs_json: String.t(), - outputs_json: String.t(), + inputs_json: [map()], + outputs_json: [map()], sweep_recipient: binary() | nil, sweep_token: binary() | nil, sweep_amount: Decimal.t() | nil @@ -56,8 +56,8 @@ defmodule Explorer.Chain.Signet.Order do field(:log_index, :integer, primary_key: true) field(:deadline, :integer) field(:block_number, :integer) - field(:inputs_json, :string) - field(:outputs_json, :string) + field(:inputs_json, :map) + field(:outputs_json, :map) field(:sweep_recipient, Hash.Address) field(:sweep_token, Hash.Address) field(:sweep_amount, Wei) diff --git a/apps/explorer/priv/contracts_abi/signet/host_orders.json b/apps/explorer/priv/contracts_abi/signet/host_orders.json index 0ea5b976ed86..d90705712317 100644 --- a/apps/explorer/priv/contracts_abi/signet/host_orders.json +++ b/apps/explorer/priv/contracts_abi/signet/host_orders.json @@ -277,4 +277,5 @@ } ] } -] \ No newline at end of file +] + diff --git a/apps/explorer/priv/contracts_abi/signet/rollup_orders.json b/apps/explorer/priv/contracts_abi/signet/rollup_orders.json index 097e8469094b..42dd68402547 100644 --- a/apps/explorer/priv/contracts_abi/signet/rollup_orders.json +++ b/apps/explorer/priv/contracts_abi/signet/rollup_orders.json @@ -537,4 +537,5 @@ } ] } -] \ No newline at end of file +] + diff --git a/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs b/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs index 39f8b1bc4741..7bb20ad82d5e 100644 --- a/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs +++ b/apps/explorer/priv/signet/migrations/20260216040000_create_signet_tables.exs @@ -13,9 +13,9 @@ defmodule Explorer.Repo.Signet.Migrations.CreateSignetTables do add(:log_index, :integer, null: false, primary_key: true) add(:deadline, :bigint, null: false) add(:block_number, :bigint, null: false) - # JSON-encoded input/output arrays for flexibility - add(:inputs_json, :text, null: false) - add(:outputs_json, :text, null: false) + # JSONB columns for input/output arrays (queryable, validated) + add(:inputs_json, :map, null: false) + add(:outputs_json, :map, null: false) # Sweep event data (nullable - only present if Sweep was emitted) add(:sweep_recipient, :bytea, null: true) add(:sweep_token, :bytea, null: true) @@ -34,7 +34,7 @@ defmodule Explorer.Repo.Signet.Migrations.CreateSignetTables do add(:transaction_hash, :bytea, null: false, primary_key: true) add(:log_index, :integer, null: false, primary_key: true) add(:block_number, :bigint, null: false) - add(:outputs_json, :text, null: false) + add(:outputs_json, :map, null: false) timestamps(null: false, type: :utc_datetime_usec) end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index f6096dc95183..38ffff7c920f 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1805,22 +1805,20 @@ defmodule Explorer.Factory do def signet_order_factory do %Order{ transaction_hash: transaction_hash(), - log_index: Enum.random(0..100), + log_index: sequence(:signet_log_index, & &1), deadline: DateTime.utc_now() |> DateTime.to_unix() |> Kernel.+(3600), block_number: block_number(), - inputs_json: - Jason.encode!([ - %{"token" => "0x" <> String.duplicate("aa", 20), "amount" => "1000000000000000000"} - ]), - outputs_json: - Jason.encode!([ - %{ - "token" => "0x" <> String.duplicate("bb", 20), - "amount" => "1000000000000000000", - "recipient" => "0x" <> String.duplicate("cc", 20), - "chainId" => 1 - } - ]) + inputs_json: [ + %{"token" => "0x" <> String.duplicate("aa", 20), "amount" => "1000000000000000000"} + ], + outputs_json: [ + %{ + "token" => "0x" <> String.duplicate("bb", 20), + "amount" => "1000000000000000000", + "recipient" => "0x" <> String.duplicate("cc", 20), + "chainId" => 1 + } + ] } end @@ -1828,17 +1826,16 @@ defmodule Explorer.Factory do %Fill{ chain_type: :rollup, transaction_hash: transaction_hash(), - log_index: Enum.random(0..100), + log_index: sequence(:signet_log_index, & &1), block_number: block_number(), - outputs_json: - Jason.encode!([ - %{ - "token" => "0x" <> String.duplicate("bb", 20), - "amount" => "1000000000000000000", - "recipient" => "0x" <> String.duplicate("cc", 20), - "chainId" => 1 - } - ]) + outputs_json: [ + %{ + "token" => "0x" <> String.duplicate("bb", 20), + "amount" => "1000000000000000000", + "recipient" => "0x" <> String.duplicate("cc", 20), + "chainId" => 1 + } + ] } end end diff --git a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex index 6027dc5ff5c5..55fe420a8e50 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/event_parser.ex @@ -145,8 +145,8 @@ defmodule Indexer.Fetcher.Signet.EventParser do block_number: block_number, transaction_hash: format_transaction_hash(log.transaction_hash), log_index: log_index, - inputs_json: Jason.encode!(format_inputs(inputs)), - outputs_json: Jason.encode!(format_outputs(outputs)) + inputs_json: format_inputs(inputs), + outputs_json: format_outputs(outputs) }} end end @@ -163,7 +163,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do block_number: block_number, transaction_hash: format_transaction_hash(log.transaction_hash), log_index: log_index, - outputs_json: Jason.encode!(format_outputs(outputs)) + outputs_json: format_outputs(outputs) }} end end @@ -211,7 +211,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do end rescue e -> - Logger.error("Error decoding Order data: #{inspect(e)}") + Logger.error("Error decoding Order data: #{inspect(e)}\n#{Exception.format_stacktrace(__STACKTRACE__)}") {:error, :decode_failed} end @@ -222,7 +222,7 @@ defmodule Indexer.Fetcher.Signet.EventParser do {:ok, decode_output_array(rest)} rescue e -> - Logger.error("Error decoding Filled data: #{inspect(e)}") + Logger.error("Error decoding Filled data: #{inspect(e)}\n#{Exception.format_stacktrace(__STACKTRACE__)}") {:error, :decode_failed} end @@ -269,22 +269,47 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp associate_sweeps_with_orders(orders, sweeps) do sweeps_by_tx = Enum.group_by(sweeps, & &1.transaction_hash) - Enum.map(orders, fn order -> - case Map.get(sweeps_by_tx, order.transaction_hash) do - [sweep] -> - attach_sweep(order, sweep) - - [_ | _] = tx_sweeps -> - # Pick the sweep with the closest log_index to the order - nearest = Enum.min_by(tx_sweeps, &abs(&1.log_index - order.log_index)) - attach_sweep(order, nearest) - - _ -> - order + # Group orders by transaction hash and match within each group + orders + |> Enum.group_by(& &1.transaction_hash) + |> Enum.flat_map(fn {tx_hash, tx_orders} -> + case Map.get(sweeps_by_tx, tx_hash) do + nil -> + tx_orders + + tx_sweeps -> + # 1:1 matching: sort both by log_index, greedily assign each order + # to the nearest unused sweep so no sweep is reused. + match_orders_to_sweeps(tx_orders, tx_sweeps) end end) end + # Greedy 1:1 matching: for each order (sorted by log_index), pick the + # closest available sweep and remove it from the pool. + defp match_orders_to_sweeps(orders, sweeps) do + sorted_orders = Enum.sort_by(orders, & &1.log_index) + available_sweeps = Enum.sort_by(sweeps, & &1.log_index) + + {matched_orders, _remaining} = + Enum.map_reduce(sorted_orders, available_sweeps, fn order, pool -> + case pool do + [] -> + {order, []} + + _ -> + {nearest, idx} = + pool + |> Enum.with_index() + |> Enum.min_by(fn {sweep, _idx} -> abs(sweep.log_index - order.log_index) end) + + {attach_sweep(order, nearest), List.delete_at(pool, idx)} + end + end) + + matched_orders + end + defp attach_sweep(order, sweep) do Map.merge(order, %{ sweep_recipient: sweep.recipient, @@ -318,13 +343,17 @@ defmodule Indexer.Fetcher.Signet.EventParser do defp format_transaction_hash("0x" <> _ = hash), do: hash defp format_transaction_hash(bytes) when is_binary(bytes), do: "0x" <> Base.encode16(bytes, case: :lower) - defp format_transaction_hash(_), do: nil + + defp format_transaction_hash(other), + do: raise(ArgumentError, "invalid transaction hash: #{inspect(other)}") # Field parsers defp decode_hex_data("0x" <> hex), do: Base.decode16!(hex, case: :mixed) defp decode_hex_data(raw) when is_binary(raw), do: raw - defp decode_hex_data(_), do: <<>> + + defp decode_hex_data(other), + do: raise(ArgumentError, "invalid hex data: #{inspect(other)}") defp decode_indexed_address("0x" <> hex) do address_hex = String.slice(hex, -40, 40) diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 510521707508..6b2c52b11577 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -415,27 +415,29 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do case chain_type do :rollup -> order_max = - Explorer.Repo.one( + Explorer.Repo.Signet.one( from(o in Order, select: max(o.block_number) ) ) fill_max = - Explorer.Repo.one( + Explorer.Repo.Signet.one( from(f in Fill, where: f.chain_type == :rollup, select: max(f.block_number) ) ) - case max(order_max, fill_max) do - nil -> default_start - block -> block + 1 + case {order_max, fill_max} do + {nil, nil} -> default_start + {nil, fill} -> fill + 1 + {order, nil} -> order + 1 + {order, fill} -> max(order, fill) + 1 end :host -> - case Explorer.Repo.one( + case Explorer.Repo.Signet.one( from(f in Fill, where: f.chain_type == :host, select: max(f.block_number) diff --git a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs index 7b19ebcc62db..9e787cfa195f 100644 --- a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs @@ -7,6 +7,27 @@ defmodule Indexer.Fetcher.Signet.AbiTest do alias Indexer.Fetcher.Signet.Abi + # Expected topic hashes computed from keccak256 of canonical event signatures + # Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) + @expected_order_topic "0x" <> + Base.encode16( + ExKeccak.hash_256( + "Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[])" + ), + case: :lower + ) + + # Filled((address,uint256,address,uint32)[]) + @expected_filled_topic "0x" <> + Base.encode16( + ExKeccak.hash_256("Filled((address,uint256,address,uint32)[])"), + case: :lower + ) + + # Sweep(address,address,uint256) + @expected_sweep_topic "0x" <> + Base.encode16(ExKeccak.hash_256("Sweep(address,address,uint256)"), case: :lower) + describe "event topic hashes" do test "event topics are different from each other" do order_topic = Abi.order_event_topic() @@ -17,6 +38,26 @@ defmodule Indexer.Fetcher.Signet.AbiTest do refute order_topic == sweep_topic refute filled_topic == sweep_topic end + + test "order event topic matches expected keccak256 hash" do + assert Abi.order_event_topic() == @expected_order_topic + end + + test "filled event topic matches expected keccak256 hash" do + assert Abi.filled_event_topic() == @expected_filled_topic + end + + test "sweep event topic matches expected keccak256 hash" do + assert Abi.sweep_event_topic() == @expected_sweep_topic + end + + test "topic hashes are valid 66-char hex strings" do + for topic <- [Abi.order_event_topic(), Abi.filled_event_topic(), Abi.sweep_event_topic()] do + assert String.length(topic) == 66 + assert String.starts_with?(topic, "0x") + assert Regex.match?(~r/^0x[0-9a-f]{64}$/, topic) + end + end end describe "rollup_orders_event_topics/0" do diff --git a/docker-compose.test.yml b/docker-compose.test.yml deleted file mode 100644 index a19af11ee31b..000000000000 --- a/docker-compose.test.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - db: - image: postgres:17 - command: postgres -c 'max_connections=250' - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: explorer_test - ports: - - "5432:5432" - tmpfs: - - /var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 2s - timeout: 5s - retries: 10 From a624bf0b3ea98d8399db66d40ccc45195c8a39e7 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 18:46:56 +0000 Subject: [PATCH 18/24] fix: update Fill schema typedoc for JSONB outputs_json field --- apps/explorer/lib/explorer/chain/signet/fill.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex index 33ea7a41c9eb..a89df31814c4 100644 --- a/apps/explorer/lib/explorer/chain/signet/fill.ex +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -30,7 +30,7 @@ defmodule Explorer.Chain.Signet.Fill do * `transaction_hash` - The hash of the transaction containing the fill (primary key) * `log_index` - The index of the log within the transaction (primary key) * `block_number` - The block number where the fill was executed - * `outputs_json` - JSON-encoded array of filled outputs (token, amount, recipient, chainId). + * `outputs_json` - List of filled outputs (token, amount, recipient, chainId) stored as JSONB. NOTE: In Filled events, the `chainId` field represents the ORIGIN chain (where the order was created), not the chain where the fill occurred. """ @@ -39,7 +39,7 @@ defmodule Explorer.Chain.Signet.Fill do block_number: non_neg_integer(), transaction_hash: binary(), log_index: non_neg_integer(), - outputs_json: String.t() + outputs_json: [map()] } @primary_key false From 92f1ff6d5f2bec24c8f47741ffe6f8e5ec67734c Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 18:54:34 +0000 Subject: [PATCH 19/24] fix: add known-good hard-coded keccak256 assertions to Abi tests Replace format-validation test with independently-computed topic hash values for stronger regression protection. Co-Authored-By: Claude Opus 4.6 --- .../test/indexer/fetcher/signet/abi_test.exs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs index 9e787cfa195f..fa46cb79a473 100644 --- a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs @@ -51,12 +51,16 @@ defmodule Indexer.Fetcher.Signet.AbiTest do assert Abi.sweep_event_topic() == @expected_sweep_topic end - test "topic hashes are valid 66-char hex strings" do - for topic <- [Abi.order_event_topic(), Abi.filled_event_topic(), Abi.sweep_event_topic()] do - assert String.length(topic) == 66 - assert String.starts_with?(topic, "0x") - assert Regex.match?(~r/^0x[0-9a-f]{64}$/, topic) - end + test "topic hashes match known-good hard-coded values" do + # These values were independently computed via keccak256 of the canonical signatures + assert Abi.order_event_topic() == + "0x80c9b8738a5ff299b770efb55e4372a5fc655294aca7145b3c529c2d89732d62" + + assert Abi.filled_event_topic() == + "0x14b3027353aba71f468d178fdede9ac211a25ae484028823bce1e6700e58e624" + + assert Abi.sweep_event_topic() == + "0xed679328aebf74ede77ae09efcf36e90244f83643dadac1c2d9f0b21a46f6ab7" end end From 0743a7ce109f99ab0ae535312c31eb4ec9f8bda9 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 19:44:46 +0000 Subject: [PATCH 20/24] fix: remove Jason.encode!/decode! from tests for native JSONB maps The Ecto type stores inputs_json/outputs_json as native Elixir maps/lists, not JSON strings. PostgreSQL JSONB handles serialization automatically, so tests should pass native data structures directly. - orders_test.exs: use raw lists instead of Jason.encode!([...]) - fills_test.exs: same - orders_fetcher_test.exs: same - event_parser_test.exs: access .inputs_json/.outputs_json directly --- .../chain/import/runner/signet/fills_test.exs | 18 ++++++------ .../import/runner/signet/orders_test.exs | 28 +++++++++---------- .../fetcher/signet/event_parser_test.exs | 10 +++---- .../fetcher/signet/orders_fetcher_test.exs | 12 ++++---- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs index a90e535a231e..126be8e13ba3 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -26,7 +26,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } ] @@ -52,7 +52,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 1, outputs_json: - Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + [%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}] } ] @@ -75,7 +75,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}] } ] @@ -87,7 +87,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do # Same log_index but different chain_type log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}] } ] @@ -120,7 +120,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}]) + [%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}] } ] @@ -133,7 +133,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do # Same log_index + chain_type log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}]) + [%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}] } ] @@ -163,7 +163,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}]) + [%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}] }, %{ chain_type: :rollup, @@ -172,7 +172,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do # Different log_index log_index: 1, outputs_json: - Jason.encode!([%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}]) + [%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}] } ] @@ -194,7 +194,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: cast_hash!(<<100 + i::256>>), log_index: 0, outputs_json: - Jason.encode!([ + [ %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} ]) } diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs index 5cf160c5fb11..e64fa86db2f6 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -25,9 +25,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } ] @@ -52,9 +52,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } ] @@ -74,9 +74,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, # Same log_index log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "2000"}]), + inputs_json: [%{"token" => "0x1234", "amount" => "2000"}], outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}]) + [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}] } ] @@ -101,9 +101,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), + inputs_json: [%{"token" => "0x1111", "amount" => "1000"}], outputs_json: - Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] }, %{ deadline: 1_700_000_001, @@ -111,9 +111,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, # Different log_index log_index: 1, - inputs_json: Jason.encode!([%{"token" => "0x4444", "amount" => "2000"}]), + inputs_json: [%{"token" => "0x4444", "amount" => "2000"}], outputs_json: - Jason.encode!([%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}]) + [%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}] } ] @@ -137,9 +137,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]), + [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}], sweep_recipient: sweep_recipient, sweep_token: sweep_token, sweep_amount: %Explorer.Chain.Wei{value: Decimal.new("12345")} @@ -162,9 +162,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100 + i, transaction_hash: cast_hash!(<<100 + i::256>>), log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}]), + inputs_json: [%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}], outputs_json: - Jason.encode!([ + [ %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} ]) } diff --git a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs index f83b6dc12803..86e8e18b764f 100644 --- a/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/event_parser_test.exs @@ -211,8 +211,8 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do assert order.block_number == 42 assert order.log_index == 3 assert order.transaction_hash == "0x" <> String.duplicate("01", 32) - assert Jason.decode!(order.inputs_json) == v.expected_inputs - assert Jason.decode!(order.outputs_json) == v.expected_outputs + assert order.inputs_json == v.expected_inputs + assert order.outputs_json == v.expected_outputs end end end @@ -275,7 +275,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do assert fill.block_number == 99 assert fill.log_index == 7 - assert Jason.decode!(fill.outputs_json) == v.expected + assert fill.outputs_json == v.expected end end end @@ -441,7 +441,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do {:ok, {[order], []}} = EventParser.parse_rollup_logs([log]) - [output] = Jason.decode!(order.outputs_json) + [output] = order.outputs_json assert output["chainId"] == max_u32 end @@ -453,7 +453,7 @@ defmodule Indexer.Fetcher.Signet.EventParserTest do {:ok, {[], [fill]}} = EventParser.parse_rollup_logs([log]) - [output] = Jason.decode!(fill.outputs_json) + [output] = fill.outputs_json assert output["amount"] == Integer.to_string(max_u256) end diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs index 28f68e1007d0..2d2c292c2c3e 100644 --- a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -60,9 +60,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1234", "amount" => "1000"}]), + inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], outputs_json: - Jason.encode!([%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } assert {:ok, %{insert_signet_orders: [order]}} = @@ -84,7 +84,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 1, outputs_json: - Jason.encode!([%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}]) + [%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}] } assert {:ok, %{insert_signet_fills: [fill]}} = @@ -103,9 +103,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: cast_hash!(<<10::256>>), log_index: 0, - inputs_json: Jason.encode!([%{"token" => "0x1111", "amount" => "1000"}]), + inputs_json: [%{"token" => "0x1111", "amount" => "1000"}], outputs_json: - Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] } fill_params = %{ @@ -114,7 +114,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: cast_hash!(<<20::256>>), log_index: 0, outputs_json: - Jason.encode!([%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}]) + [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] } assert {:ok, result} = From b00bea2aff7dfd3b16b9f6350d26172d6c73ba02 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 19:44:54 +0000 Subject: [PATCH 21/24] docs: document why :map Ecto type works for JSONB arrays PostgreSQL JSONB stores both maps and arrays natively. The :map Ecto type works here because: 1. JSONB handles serialization of any JSON-compatible structure 2. insert_all bypasses Ecto changesets, so type validation is lenient 3. The data flows in as lists from EventParser and out as lists to queries --- apps/explorer/lib/explorer/chain/signet/fill.ex | 2 ++ apps/explorer/lib/explorer/chain/signet/order.ex | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/signet/fill.ex b/apps/explorer/lib/explorer/chain/signet/fill.ex index a89df31814c4..4cc787a7ac3a 100644 --- a/apps/explorer/lib/explorer/chain/signet/fill.ex +++ b/apps/explorer/lib/explorer/chain/signet/fill.ex @@ -31,6 +31,8 @@ defmodule Explorer.Chain.Signet.Fill do * `log_index` - The index of the log within the transaction (primary key) * `block_number` - The block number where the fill was executed * `outputs_json` - List of filled outputs (token, amount, recipient, chainId) stored as JSONB. + Note: Uses `:map` Ecto type which accepts both maps and lists - PostgreSQL JSONB + stores both natively, and `insert_all` bypasses changeset validation. NOTE: In Filled events, the `chainId` field represents the ORIGIN chain (where the order was created), not the chain where the fill occurred. """ diff --git a/apps/explorer/lib/explorer/chain/signet/order.ex b/apps/explorer/lib/explorer/chain/signet/order.ex index ce16ed1a5dd5..792155e068c3 100644 --- a/apps/explorer/lib/explorer/chain/signet/order.ex +++ b/apps/explorer/lib/explorer/chain/signet/order.ex @@ -30,8 +30,11 @@ defmodule Explorer.Chain.Signet.Order do * `log_index` - The index of the log within the transaction (primary key) * `deadline` - The deadline timestamp for the order * `block_number` - The block number where the order was created - * `inputs_json` - List of input tokens and amounts (stored as JSONB) + * `inputs_json` - List of input tokens and amounts (stored as JSONB). + Note: Uses `:map` Ecto type which accepts both maps and lists - PostgreSQL JSONB + stores both natively, and `insert_all` bypasses changeset validation. * `outputs_json` - List of output tokens, amounts, recipients, and chainIds (stored as JSONB). + Note: Uses `:map` Ecto type (same rationale as inputs_json). NOTE: In Order events, the `chainId` field represents the DESTINATION chain (where assets should be delivered), not the chain where the order was created. * `sweep_recipient` - Recipient address from Sweep event (if any) From 52a92265db8c25eb02e8c26d62d5a0dc563314ad Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 19:56:20 +0000 Subject: [PATCH 22/24] fix: address PR review feedback (syntax errors, missing alias) --- .../explorer/chain/import/runner/signet/fills_test.exs | 2 +- .../explorer/chain/import/runner/signet/orders_test.exs | 2 +- apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs index 126be8e13ba3..ba49b0cb9530 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -196,7 +196,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do outputs_json: [ %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} - ]) + ] } end diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs index e64fa86db2f6..901f9249ed96 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -166,7 +166,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do outputs_json: [ %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} - ]) + ] } end diff --git a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex index 6b2c52b11577..b647baa38264 100644 --- a/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/signet/orders_fetcher.ex @@ -53,6 +53,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do alias Explorer.Chain alias Explorer.Chain.Signet.{Fill, Order} + alias Explorer.Repo.Signet, as: SignetRepo alias Indexer.BufferedTask alias Indexer.Fetcher.Signet.{Abi, EventParser} alias Indexer.Helper, as: IndexerHelper @@ -415,14 +416,14 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do case chain_type do :rollup -> order_max = - Explorer.Repo.Signet.one( + SignetRepo.one( from(o in Order, select: max(o.block_number) ) ) fill_max = - Explorer.Repo.Signet.one( + SignetRepo.one( from(f in Fill, where: f.chain_type == :rollup, select: max(f.block_number) @@ -437,7 +438,7 @@ defmodule Indexer.Fetcher.Signet.OrdersFetcher do end :host -> - case Explorer.Repo.Signet.one( + case SignetRepo.one( from(f in Fill, where: f.chain_type == :host, select: max(f.block_number) From ce4ef9f218aa1f64f40645568e4e658a33ee4063 Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 20:04:41 +0000 Subject: [PATCH 23/24] style: run mix format --- .../chain/import/runner/signet/fills_test.exs | 31 +++++++------------ .../import/runner/signet/orders_test.exs | 25 ++++++--------- .../test/indexer/fetcher/signet/abi_test.exs | 4 +-- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs index ba49b0cb9530..cea9ae039021 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/fills_test.exs @@ -25,8 +25,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: - [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } ] @@ -51,8 +50,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 200, transaction_hash: tx_hash, log_index: 1, - outputs_json: - [%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}] + outputs_json: [%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}] } ] @@ -74,8 +72,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: - [%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}] } ] @@ -86,8 +83,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, # Same log_index but different chain_type log_index: 0, - outputs_json: - [%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x1111", "recipient" => "0x2222", "amount" => "500", "chainId" => "1"}] } ] @@ -119,8 +115,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: - [%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}] + outputs_json: [%{"token" => "0x1234", "amount" => "500", "recipient" => "0x5678", "chainId" => "1"}] } ] @@ -132,8 +127,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, # Same log_index + chain_type log_index: 0, - outputs_json: - [%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}] + outputs_json: [%{"token" => "0x1234", "amount" => "1000", "recipient" => "0x5678", "chainId" => "1"}] } ] @@ -162,8 +156,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100, transaction_hash: tx_hash, log_index: 0, - outputs_json: - [%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}] + outputs_json: [%{"token" => "0x1111", "amount" => "500", "recipient" => "0x2222", "chainId" => "1"}] }, %{ chain_type: :rollup, @@ -171,8 +164,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, # Different log_index log_index: 1, - outputs_json: - [%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}] + outputs_json: [%{"token" => "0x3333", "amount" => "1000", "recipient" => "0x4444", "chainId" => "1"}] } ] @@ -193,10 +185,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 100 + i, transaction_hash: cast_hash!(<<100 + i::256>>), log_index: 0, - outputs_json: - [ - %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} - ] + outputs_json: [ + %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} + ] } end diff --git a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs index 901f9249ed96..1846248b1216 100644 --- a/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/signet/orders_test.exs @@ -26,8 +26,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], - outputs_json: - [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } ] @@ -53,8 +52,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], - outputs_json: - [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } ] @@ -75,8 +73,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do # Same log_index log_index: 0, inputs_json: [%{"token" => "0x1234", "amount" => "2000"}], - outputs_json: - [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}] + outputs_json: [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "1000", "chainId" => "1"}] } ] @@ -102,8 +99,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, inputs_json: [%{"token" => "0x1111", "amount" => "1000"}], - outputs_json: - [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] }, %{ deadline: 1_700_000_001, @@ -112,8 +108,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do # Different log_index log_index: 1, inputs_json: [%{"token" => "0x4444", "amount" => "2000"}], - outputs_json: - [%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}] + outputs_json: [%{"token" => "0x5555", "recipient" => "0x6666", "amount" => "1000", "chainId" => "1"}] } ] @@ -138,8 +133,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], - outputs_json: - [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}], + outputs_json: [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}], sweep_recipient: sweep_recipient, sweep_token: sweep_token, sweep_amount: %Explorer.Chain.Wei{value: Decimal.new("12345")} @@ -163,10 +157,9 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: cast_hash!(<<100 + i::256>>), log_index: 0, inputs_json: [%{"token" => "0x#{i}", "amount" => "#{i * 1000}"}], - outputs_json: - [ - %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} - ] + outputs_json: [ + %{"token" => "0x#{i}", "recipient" => "0x#{i}", "amount" => "#{i * 500}", "chainId" => "1"} + ] } end diff --git a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs index fa46cb79a473..a166bea9f1f2 100644 --- a/apps/indexer/test/indexer/fetcher/signet/abi_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/abi_test.exs @@ -11,9 +11,7 @@ defmodule Indexer.Fetcher.Signet.AbiTest do # Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[]) @expected_order_topic "0x" <> Base.encode16( - ExKeccak.hash_256( - "Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[])" - ), + ExKeccak.hash_256("Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[])"), case: :lower ) From da83141b861c7c539a51e10efc422357a19e599d Mon Sep 17 00:00:00 2001 From: init4samwise Date: Mon, 2 Mar 2026 20:11:30 +0000 Subject: [PATCH 24/24] style: run mix format on entire project --- .../indexer/fetcher/signet/orders_fetcher_test.exs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs index 2d2c292c2c3e..7e3f33f3936e 100644 --- a/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs +++ b/apps/indexer/test/indexer/fetcher/signet/orders_fetcher_test.exs @@ -61,8 +61,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: tx_hash, log_index: 0, inputs_json: [%{"token" => "0x1234", "amount" => "1000"}], - outputs_json: - [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x5678", "recipient" => "0x9abc", "amount" => "500", "chainId" => "1"}] } assert {:ok, %{insert_signet_orders: [order]}} = @@ -83,8 +82,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 150, transaction_hash: tx_hash, log_index: 1, - outputs_json: - [%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}] + outputs_json: [%{"token" => "0xaaaa", "recipient" => "0xbbbb", "amount" => "1000", "chainId" => "1"}] } assert {:ok, %{insert_signet_fills: [fill]}} = @@ -104,8 +102,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do transaction_hash: cast_hash!(<<10::256>>), log_index: 0, inputs_json: [%{"token" => "0x1111", "amount" => "1000"}], - outputs_json: - [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] } fill_params = %{ @@ -113,8 +110,7 @@ if Application.compile_env(:explorer, :chain_type) == :signet do block_number: 200, transaction_hash: cast_hash!(<<20::256>>), log_index: 0, - outputs_json: - [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] + outputs_json: [%{"token" => "0x2222", "recipient" => "0x3333", "amount" => "500", "chainId" => "1"}] } assert {:ok, result} =