Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Blockscout Development Guidelines

## Pre-commit Requirements

You MUST run the following linting steps before committing any changes:

1. **Code formatting**: `mix format --check-formatted`
2. **Credo**: `mix credo`
3. **Dialyzer**: `mix dialyzer --halt-exit-status`
- If your change is chain-type specific, also run with the appropriate `CHAIN_TYPE` env var (e.g. `CHAIN_TYPE=signet`)
4. **Sobelow** (if you changed files in `apps/explorer` or `apps/block_scout_web`):
- `cd apps/explorer && mix sobelow --config`
- `cd apps/block_scout_web && mix sobelow --config`
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
defmodule BlockScoutWeb.API.V2.Signet.SignetController do
@moduledoc """
Controller for Signet order and fill API endpoints.

Provides endpoints for querying Signet orders and fills by block number
or transaction hash.
"""
use BlockScoutWeb, :controller

import BlockScoutWeb.Chain,
only: [
next_page_params: 5,
paging_options: 1,
split_list_by_page: 1
]

alias BlockScoutWeb.API.V2.Signet.SignetView
alias Explorer.Chain.Signet

@api_true [api?: true]

action_fallback(BlockScoutWeb.API.V2.FallbackController)

@doc """
GET /api/v2/signet/blocks/:block_number/orders

Returns all orders initiated in a specific block.
"""
def block_orders(conn, %{"block_number" => block_number_string} = params) do
with {:ok, block_number} <- parse_block_number(block_number_string) do
full_options =
@api_true
|> Keyword.merge(paging_options(params))

orders_plus_one = Signet.orders_by_block(block_number, full_options)
{orders, next_page} = split_list_by_page(orders_plus_one)

next_page_params =
next_page_params(next_page, orders, params, false, &signet_paging_options/1)

conn
|> put_status(200)
|> put_view(SignetView)
|> render(:orders, %{orders: orders, next_page_params: next_page_params})
end
end

@doc """
GET /api/v2/signet/blocks/:block_number/fills

Returns all fills executed in a specific block.
"""
def block_fills(conn, %{"block_number" => block_number_string} = params) do
with {:ok, block_number} <- parse_block_number(block_number_string) do
full_options =
@api_true
|> Keyword.merge(paging_options(params))

fills_plus_one = Signet.fills_by_block(block_number, full_options)
{fills, next_page} = split_list_by_page(fills_plus_one)

next_page_params =
next_page_params(next_page, fills, params, false, &signet_paging_options/1)

conn
|> put_status(200)
|> put_view(SignetView)
|> render(:fills, %{fills: fills, next_page_params: next_page_params})
end
end

@doc """
GET /api/v2/signet/blocks/:block_number/activity

Returns combined view of orders and fills for a specific block.
"""
def block_activity(conn, %{"block_number" => block_number_string} = params) do
with {:ok, block_number} <- parse_block_number(block_number_string) do
full_options =
@api_true
|> Keyword.merge(paging_options(params))

orders = Signet.orders_by_block(block_number, full_options)
fills = Signet.fills_by_block(block_number, full_options)

conn
|> put_status(200)
|> put_view(SignetView)
|> render(:activity, %{orders: orders, fills: fills, next_page_params: nil})
end
end

@doc """
GET /api/v2/signet/transactions/:transaction_hash/orders

Returns orders initiated by a specific transaction.
"""
def transaction_orders(conn, %{"transaction_hash" => tx_hash_string} = params) do
with {:ok, tx_hash} <- parse_transaction_hash(tx_hash_string) do
full_options =
@api_true
|> Keyword.merge(paging_options(params))

orders_plus_one = Signet.orders_by_transaction(tx_hash, full_options)
{orders, next_page} = split_list_by_page(orders_plus_one)

next_page_params =
next_page_params(next_page, orders, params, false, &signet_paging_options/1)

conn
|> put_status(200)
|> put_view(SignetView)
|> render(:orders, %{orders: orders, next_page_params: next_page_params})
end
end

@doc """
GET /api/v2/signet/transactions/:transaction_hash/fills

Returns fills executed by a specific transaction.
"""
def transaction_fills(conn, %{"transaction_hash" => tx_hash_string} = params) do
with {:ok, tx_hash} <- parse_transaction_hash(tx_hash_string) do
full_options =
@api_true
|> Keyword.merge(paging_options(params))

fills_plus_one = Signet.fills_by_transaction(tx_hash, full_options)
{fills, next_page} = split_list_by_page(fills_plus_one)

next_page_params =
next_page_params(next_page, fills, params, false, &signet_paging_options/1)

conn
|> put_status(200)
|> put_view(SignetView)
|> render(:fills, %{fills: fills, next_page_params: next_page_params})
end
end

@doc """
GET /api/v2/signet/transactions/:transaction_hash/activity

Returns combined view of orders and fills for a specific transaction.
"""
def transaction_activity(conn, %{"transaction_hash" => tx_hash_string} = params) do
with {:ok, tx_hash} <- parse_transaction_hash(tx_hash_string) do
full_options =
@api_true
|> Keyword.merge(paging_options(params))

orders = Signet.orders_by_transaction(tx_hash, full_options)
fills = Signet.fills_by_transaction(tx_hash, full_options)

conn
|> put_status(200)
|> put_view(SignetView)
|> render(:activity, %{orders: orders, fills: fills, next_page_params: nil})
end
end

# Private functions

defp parse_block_number(block_number_string) do
case Integer.parse(block_number_string) do
{block_number, ""} when block_number >= 0 -> {:ok, block_number}
_ -> {:error, {:invalid, :number}}
end
end

defp parse_transaction_hash(tx_hash_string) do
case Explorer.Chain.Hash.Full.cast(tx_hash_string) do
{:ok, hash} -> {:ok, hash}
:error -> {:error, {:invalid, :hash}}
end
end

defp signet_paging_options(item) do
%{
"log_index" => item.log_index
}
end
end
14 changes: 14 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,20 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
end
end

scope "/signet" do
scope "/blocks/:block_number" do
get("/orders", V2.Signet.SignetController, :block_orders)
get("/fills", V2.Signet.SignetController, :block_fills)
get("/activity", V2.Signet.SignetController, :block_activity)
end

scope "/transactions/:transaction_hash" do
get("/orders", V2.Signet.SignetController, :transaction_orders)
get("/fills", V2.Signet.SignetController, :transaction_fills)
get("/activity", V2.Signet.SignetController, :transaction_activity)
end
end

scope "/addresses" do
get("/", V2.AddressController, :addresses_list)
get("/:address_hash_param", V2.AddressController, :address)
Expand Down
137 changes: 137 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/views/api/v2/signet_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule BlockScoutWeb.API.V2.Signet.SignetView do
@moduledoc """
View for rendering Signet order and fill API responses.
"""
use BlockScoutWeb, :view

alias Explorer.Chain.Signet
alias Explorer.Chain.Signet.{Fill, Order}

@doc """
Renders a list of orders.
"""
def render("orders.json", %{orders: orders, next_page_params: next_page_params}) do
%{
"items" => Enum.map(orders, &prepare_order/1),
"next_page_params" => next_page_params
}
end

@doc """
Renders a list of fills.
"""
def render("fills.json", %{fills: fills, next_page_params: next_page_params}) do
%{
"items" => Enum.map(fills, &prepare_fill/1),
"next_page_params" => next_page_params
}
end

@doc """
Renders combined activity (orders and fills).
"""
def render("activity.json", %{orders: orders, fills: fills, next_page_params: next_page_params}) do
%{
"orders" => Enum.map(orders, &prepare_order/1),
"fills" => Enum.map(fills, &prepare_fill/1),
"next_page_params" => next_page_params
}
end

@doc """
Prepares an order for JSON rendering.
"""
@spec prepare_order(Order.t()) :: map()
def prepare_order(%Order{} = order) do
%{
"order_hash" => compute_order_hash(order),
"outputs_witness_hash" => compute_outputs_witness_hash(order.outputs_json),
"deadline" => order.deadline,
"transaction_hash" => to_string(order.transaction_hash),
"log_index" => order.log_index,
"block_number" => order.block_number,
"inputs" => prepare_inputs(order.inputs_json),
"outputs" => prepare_outputs(order.outputs_json),
"status" => to_string(Signet.calculate_order_status(order)),
"sweep" => prepare_sweep(order)
}
end

@doc """
Prepares a fill for JSON rendering.
"""
@spec prepare_fill(Fill.t()) :: map()
def prepare_fill(%Fill{} = fill) do
%{
"outputs_witness_hash" => compute_outputs_witness_hash(fill.outputs_json),
"transaction_hash" => to_string(fill.transaction_hash),
"log_index" => fill.log_index,
"block_number" => fill.block_number,
"chain_type" => to_string(fill.chain_type),
"outputs" => prepare_outputs(fill.outputs_json)
}
end

# Private helper functions

defp prepare_inputs(nil), do: []

defp prepare_inputs(inputs) when is_list(inputs) do
Enum.map(inputs, fn input ->
%{
"token" => Map.get(input, "token") || Map.get(input, :token),
"amount" => to_string(Map.get(input, "amount") || Map.get(input, :amount))
}
end)
end

defp prepare_inputs(_), do: []

defp prepare_outputs(nil), do: []

defp prepare_outputs(outputs) when is_list(outputs) do
Enum.map(outputs, fn output ->
%{
"token" => Map.get(output, "token") || Map.get(output, :token),
"amount" => to_string(Map.get(output, "amount") || Map.get(output, :amount)),
"recipient" => Map.get(output, "recipient") || Map.get(output, :recipient),
"chain_id" =>
Map.get(output, "chainId") || Map.get(output, :chainId) || Map.get(output, "chain_id") ||
Map.get(output, :chain_id)
}
end)
end

defp prepare_outputs(_), do: []

defp prepare_sweep(%Order{sweep_recipient: nil}), do: nil

defp prepare_sweep(%Order{sweep_recipient: recipient, sweep_token: token, sweep_amount: amount}) do
%{
"recipient" => to_string(recipient),
"token" => if(token, do: to_string(token), else: nil),
"amount" => if(amount, do: to_string(amount), else: nil)
}
end

# Compute a hash for the order - using transaction_hash + log_index as identifier
defp compute_order_hash(%Order{transaction_hash: tx_hash, log_index: log_index}) do
"#{tx_hash}:#{log_index}"
end

# Compute outputs witness hash - this is a placeholder
# In a real implementation, this would compute the actual witness hash
defp compute_outputs_witness_hash(nil), do: nil

defp compute_outputs_witness_hash(outputs) when is_list(outputs) do
# For now, return a deterministic hash based on the outputs
# A proper implementation would use the actual cryptographic hash
outputs
|> :erlang.term_to_binary()
|> then(fn bin -> :crypto.hash(:sha256, bin) end)
|> Base.encode16(case: :lower)
|> then(fn hash -> "0x" <> hash end)
end

defp compute_outputs_witness_hash(_), do: nil
end
18 changes: 18 additions & 0 deletions apps/explorer/lib/explorer/chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,24 @@ defmodule Explorer.Chain do
where(query, [block], block.number < ^block_number)
end

@doc """
Paginates Signet orders by log_index.
"""
def page_signet_orders(query, %PagingOptions{key: nil}), do: query

def page_signet_orders(query, %PagingOptions{key: {log_index}}) do
where(query, [o], o.log_index > ^log_index)
end

@doc """
Paginates Signet fills by log_index.
"""
def page_signet_fills(query, %PagingOptions{key: nil}), do: query

def page_signet_fills(query, %PagingOptions{key: {log_index}}) do
where(query, [f], f.log_index > ^log_index)
end

defp page_logs(query, %PagingOptions{key: nil}), do: query

defp page_logs(query, %PagingOptions{key: {index}}) do
Expand Down
Loading
Loading