Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
50785d6
feat: add Replica1 database config to runtime.exs for read replica su…
Errorist79 Nov 11, 2025
fe5933e
feat: add Replica1 to ecto_repos list when DATABASE_READ_ONLY_API_URL…
Errorist79 Nov 11, 2025
0f8f39f
fix: keep Replica1 out of ecto_repos to avoid migration attempts on r…
Errorist79 Nov 11, 2025
4b30d19
feat: add read-only replica support with migration filtering
Errorist79 Nov 11, 2025
65a3bd4
fix: call repo.config() as function not property
Errorist79 Nov 12, 2025
a94f2e9
refactor: remove Replica1 from ecto_repos to avoid migrations
Errorist79 Nov 12, 2025
d7939d6
feat: conditionally add Replica1 to ecto_repos
Errorist79 Nov 12, 2025
a3f9385
fix: add Replica1 to ecto_repos in runtime.exs
Errorist79 Nov 12, 2025
f8e83b0
refactor: clean up config_helper.exs by removing commented notes on R…
Errorist79 Nov 12, 2025
ce36f71
fix: check RELEASE_COMMAND equals 'migrate' instead of checking if it…
Errorist79 Nov 12, 2025
735f26c
refactor: simplify read-only replica handling with static ecto_repos
Errorist79 Nov 12, 2025
ba92c55
feat: timescaledb support
Errorist79 Nov 13, 2025
43bbb9b
refactor: update conflict resolution for internal transactions
Errorist79 Nov 13, 2025
5c96033
refactor: update conflict resolution for Citus distributed table support
Errorist79 Nov 13, 2025
bcb576c
refactor: disable reward deletion for Monad blockchain compatibility
Errorist79 Nov 13, 2025
be2c3e5
fix: Complete Citus distributed table setup and resolve schema conflicts
Errorist79 Nov 14, 2025
0cf3326
docs: add changes docs
Errorist79 Nov 14, 2025
af3d79a
fix: update conflict_target and sorting for transaction_forks to alig…
Errorist79 Nov 14, 2025
ab6e674
fix: update on_conflict strategy for Citus compatibility
Errorist79 Nov 14, 2025
397e5d8
fix: refine on_conflict strategy for Citus compatibility
Errorist79 Nov 14, 2025
2d8e762
fix: update on_conflict strategy and conflict_target for Citus compat…
Errorist79 Nov 14, 2025
db96d8b
fix: remove explicit locking in fork_transactions for Citus compatibi…
Errorist79 Nov 14, 2025
9f572ea
refactor: rewrite fork_transactions for Citus compatibility
Errorist79 Nov 14, 2025
09f64ea
refactor: optimize transaction updates for Citus compatibility
Errorist79 Nov 14, 2025
610be77
refactor: simplify delete_related_transaction_operations for Citus co…
Errorist79 Nov 14, 2025
7e8af29
feat: enable sequential mode for Citus reference table updates
Errorist79 Nov 14, 2025
a05d5a3
refactor: ensure sequential mode query runs in transaction for Citus …
Errorist79 Nov 14, 2025
38c7453
Fix Citus-incompatible query patterns to resolve PostgreSQL restart i…
Errorist79 Nov 14, 2025
5d13a47
Fix addresses.ex FOR NO KEY UPDATE causing ERROR 0A000 in production
Errorist79 Nov 14, 2025
0e29234
Fix Ecto.QueryError: remove order_by from update_all query
Errorist79 Nov 14, 2025
f467d0f
Fix ERROR XX000: Add token-level granular advisory locks
Errorist79 Nov 14, 2025
6539c58
fix(indexer): gracefully handle fetch_codes partial errors without cr…
Errorist79 Nov 27, 2025
2a18dc6
Enable sequential mode for Citus reference table inserts to prevent p…
Errorist79 Nov 28, 2025
b444d57
Enable sequential mode for Citus reference table inserts to prevent p…
Errorist79 Nov 28, 2025
734836d
fix(indexer): use configured cors
Errorist79 Nov 28, 2025
16d991b
fix(indexer): use configured cors
Errorist79 Nov 28, 2025
aca8940
refactor(endpoint): replace CORSPlug with DynamicCORS for improved or…
Errorist79 Nov 28, 2025
fb50e4c
feat(indexer): add on-demand block and transaction fetchers
Errorist79 Dec 1, 2025
e474129
feat(indexer): enhance on-demand block fetching with round-robin RPC …
Errorist79 Dec 2, 2025
0cae357
fix(indexer): update block fetcher to accept a list of RPC URLs
Errorist79 Dec 2, 2025
cae1691
refactor(indexer): enhance transaction fetching with round-robin RPC …
Errorist79 Dec 2, 2025
501bd21
refactor(indexer): enhance block transformation with consensus flag
Errorist79 Dec 2, 2025
985ae44
refactor(indexer): update fetch_receipts to accept full transaction p…
Errorist79 Dec 2, 2025
27869ac
refactor(indexer): improve on-demand fetch logic with retry mechanism
Errorist79 Dec 2, 2025
26ff865
fix(indexer): batch receipt fetching to avoid RPC batch size limits
Errorist79 Dec 2, 2025
e646698
refactor(indexer): use Block.Fetcher Receipts module for on-demand fetch
Errorist79 Dec 2, 2025
b424552
perf(indexer): reduce batch size to 5 for Monad RPC limits
Errorist79 Dec 2, 2025
01a3b87
refactor(indexer): make receipt fetching batch size and concurrency c…
Errorist79 Dec 2, 2025
d416b85
Merge branch 'master' into feat/on-demand-blocks-txs
Errorist79 Dec 2, 2025
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
459 changes: 459 additions & 0 deletions MONAD_FORK_CHANGES.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ defmodule BlockScoutWeb.API.V2.BlockController do
import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
import Explorer.Chain.Address.Reputation, only: [reputation_association: 0]

alias BlockScoutWeb.AccessHelper
alias BlockScoutWeb.API.V2.{
Ethereum.DepositController,
Ethereum.DepositView,
TransactionView,
WithdrawalView
}
alias Indexer.Fetcher.OnDemand.Block, as: BlockOnDemand

alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse
alias Explorer.Chain
Expand Down Expand Up @@ -159,11 +161,11 @@ defmodule BlockScoutWeb.API.V2.BlockController do
Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param` endpoint.
"""
@spec block(Plug.Conn.t(), map()) ::
{:error, :not_found | {:invalid, :hash | :number}}
{:error, :not_found | {:invalid, :hash | :number} | :rate_limited}
| {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}
| Plug.Conn.t()
def block(conn, %{block_hash_or_number_param: block_hash_or_number}) do
with {:ok, block} <- block_param_to_block(block_hash_or_number, @block_params) do
with {:ok, block} <- block_param_to_block(block_hash_or_number, @block_params, conn) do
conn
|> put_status(200)
|> render(:block, %{block: block})
Expand All @@ -179,8 +181,16 @@ defmodule BlockScoutWeb.API.V2.BlockController do
{:ok, _block} = ok_response ->
ok_response

_ ->
{:lost_consensus, Chain.nonconsensus_block_by_number(number, @api_true)}
{:error, :not_found} ->
# Check if block exists but lost consensus
case Chain.nonconsensus_block_by_number(number, @api_true) do
{:ok, _block} = lost_consensus_block ->
{:lost_consensus, lost_consensus_block}

{:error, :not_found} ->
# Block doesn't exist at all - allow on-demand fetch
{:error, :not_found}
end
end
end

Expand Down Expand Up @@ -692,9 +702,53 @@ defmodule BlockScoutWeb.API.V2.BlockController do
end
end

defp block_param_to_block(block_hash_or_number, options \\ @api_true) do
defp block_param_to_block(block_hash_or_number, options \\ @api_true, conn \\ nil) do
with {:ok, type, value} <- parse_block_hash_or_number_param(block_hash_or_number) do
fetch_block(type, value, options)
case fetch_block(type, value, options) do
{:ok, _block} = result ->
result

{:error, :not_found} ->
# Try on-demand fetch
try_on_demand_block_fetch(type, value, options, conn)

{:lost_consensus, _} = result ->
result
end
end
end

defp try_on_demand_block_fetch(_type, _value, _options, nil), do: {:error, :not_found}

defp try_on_demand_block_fetch(:hash, hash, options, conn) do
ip = AccessHelper.conn_to_ip_string(conn)

case BlockOnDemand.fetch_by_hash(ip, hash) do
{:ok, _block} ->
# Re-fetch with full associations
Chain.hash_to_block(hash, options)

{:error, :rate_limited} ->
{:error, :rate_limited}

{:error, _} ->
{:error, :not_found}
end
end

defp try_on_demand_block_fetch(:number, number, options, conn) do
ip = AccessHelper.conn_to_ip_string(conn)

case BlockOnDemand.fetch_by_number(ip, number) do
{:ok, block} ->
# Re-fetch with full associations
Chain.hash_to_block(block.hash, options)

{:error, :rate_limited} ->
{:error, :rate_limited}

{:error, _} ->
{:error, :not_found}
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ defmodule BlockScoutWeb.API.V2.FallbackController do
|> call({:not_found, nil})
end

def call(conn, {:error, :rate_limited}) do
Logger.warning(fn ->
["On-demand fetch rate limited"]
end)

conn
|> put_status(:too_many_requests)
|> put_view(ApiView)
|> render(:message, %{message: "Too many on-demand fetch requests. Please try again later."})
end

def call(conn, {:error, %Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
alias Explorer.Chain.ZkSync.Reader, as: ZkSyncReader
alias Indexer.Fetcher.OnDemand.FirstTrace, as: FirstTraceOnDemand
alias Indexer.Fetcher.OnDemand.NeonSolanaTransactions, as: NeonSolanaTransactions
alias Indexer.Fetcher.OnDemand.Transaction, as: TransactionOnDemand

action_fallback(BlockScoutWeb.API.V2.FallbackController)

Expand Down Expand Up @@ -194,7 +195,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
[necessity_by_association: necessity_by_association]
|> Keyword.merge(@api_true)

with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params, options),
with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params, options, conn),
preloaded <-
Chain.preload_token_transfers(
transaction,
Expand Down Expand Up @@ -1181,13 +1182,41 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
| {:not_found, {:error, :not_found}}
| {:restricted_access, true}
| {:ok, Transaction.t(), Hash.t()}
def validate_transaction(transaction_hash_string, params, options \\ @api_true) do
def validate_transaction(transaction_hash_string, params, options \\ @api_true, conn \\ nil) do
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_full_hash(transaction_hash_string)},
{:not_found, {:ok, transaction}} <-
{:not_found, Chain.hash_to_transaction(transaction_hash, options)},
{:not_found, fetch_or_demand_transaction(transaction_hash, options, conn)},
{:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params),
{:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do
{:ok, transaction, transaction_hash}
end
end

defp fetch_or_demand_transaction(transaction_hash, options, conn) do
case Chain.hash_to_transaction(transaction_hash, options) do
{:ok, _transaction} = result ->
result

{:error, :not_found} ->
try_on_demand_transaction_fetch(transaction_hash, options, conn)
end
end

defp try_on_demand_transaction_fetch(_transaction_hash, _options, nil), do: {:error, :not_found}

defp try_on_demand_transaction_fetch(transaction_hash, options, conn) do
ip = AccessHelper.conn_to_ip_string(conn)

case TransactionOnDemand.fetch_by_hash(ip, transaction_hash) do
{:ok, _transaction} ->
# Re-fetch with full associations
Chain.hash_to_transaction(transaction_hash, options)

{:error, :rate_limited} ->
{:error, :rate_limited}

{:error, _} ->
{:error, :not_found}
end
end
end
3 changes: 2 additions & 1 deletion apps/block_scout_web/lib/block_scout_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ defmodule BlockScoutWeb.Endpoint do

# 'x-apollo-tracing' header for https://www.graphqlbin.com to work with our GraphQL endpoint
# 'updated-gas-oracle' header for /api/v2/stats endpoint, added to support cross-origin requests (e.g. multichain search explorer)
plug(CORSPlug,
# CORS origin can be configured via API_V2_CORS_ALLOWED_ORIGIN env var (supports comma-separated multiple origins)
plug(BlockScoutWeb.Plugs.DynamicCORS,
headers:
[
"x-apollo-tracing",
Expand Down
39 changes: 39 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/plugs/dynamic_cors.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule BlockScoutWeb.Plugs.DynamicCORS do
@moduledoc """
A wrapper around CORSPlug that reads the allowed origin from
API_V2_CORS_ALLOWED_ORIGIN environment variable at runtime.

Supports:
- Single origin: "https://example.com"
- Multiple origins (comma-separated): "https://example1.com,https://example2.com"
- Wildcard: "*" (default if not set)
"""

@behaviour Plug

@impl true
def init(opts), do: opts

@impl true
def call(conn, opts) do
origin = get_cors_origin()
cors_opts = Keyword.put(opts, :origin, origin)
CORSPlug.call(conn, CORSPlug.init(cors_opts))
end

defp get_cors_origin do
case System.get_env("API_V2_CORS_ALLOWED_ORIGIN") do
nil -> "*"
"" -> "*"
"*" -> "*"
origins ->
origins
|> String.split(",")
|> Enum.map(&String.trim/1)
|> case do
[single] -> single
multiple -> multiple
end
end
end
end
16 changes: 7 additions & 9 deletions apps/explorer/lib/explorer/chain/import/runner/addresses.ex
Original file line number Diff line number Diff line change
Expand Up @@ -264,18 +264,16 @@ defmodule Explorer.Chain.Import.Runner.Addresses do
if Enum.empty?(ordered_created_contract_hashes) do
{:ok, []}
else
query =
from(t in Transaction,
where: t.created_contract_address_hash in ^ordered_created_contract_hashes,
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
order_by: t.hash,
lock: "FOR NO KEY UPDATE"
)

# Citus-compatible: Remove subquery JOIN and FOR NO KEY UPDATE lock
# transactions is distributed by hash - FOR NO KEY UPDATE causes 0A000 errors
# Direct WHERE IN is more efficient than subquery pattern
# Note: order_by is NOT supported in update_all, removed from query
try do
{_, result} =
repo.update_all(
from(t in Transaction, join: s in subquery(query), on: t.hash == s.hash),
from(t in Transaction,
where: t.created_contract_address_hash in ^ordered_created_contract_hashes
),
[set: [created_contract_code_indexed_at: timestamps.updated_at]],
timeout: timeout
)
Expand Down
Loading
Loading