Skip to content

Commit 97889f1

Browse files
authored
Merge pull request #7 from maestro-org/perf/api-performance-improvement
perf/api-performance-improvement
2 parents dacc801 + 684dcf6 commit 97889f1

File tree

6 files changed

+184
-46
lines changed

6 files changed

+184
-46
lines changed

apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do
1414
import BlockScoutWeb.PagingHelper,
1515
only: [
1616
delete_parameters_from_next_page_params: 1,
17-
token_transfers_types_options: 1
17+
token_transfers_types_options: 1,
18+
token_transfers_activity_options: 1
1819
]
1920

2021
import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1]
@@ -41,6 +42,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do
4142
%PagingOptions{paging_options | page_size: page_size + 1}
4243
end)
4344
|> Keyword.merge(token_transfers_types_options(params))
45+
|> Keyword.merge(token_transfers_activity_options(params))
4446
|> Keyword.merge(@api_true)
4547

4648
result =

apps/block_scout_web/lib/block_scout_web/paging_helper.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ defmodule BlockScoutWeb.PagingHelper do
3737
end
3838

3939
@allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"]
40+
@allowed_token_transfer_activity_labels ["MINTING", "BURNING", "TRANSFER", "SPAWNING"]
4041
@allowed_nft_type_labels ["ERC-721", "ERC-1155", "ERC-404"]
4142
@allowed_chain_id [1, 56, 99]
4243
@allowed_stability_validators_states ["active", "probation", "inactive"]
@@ -81,6 +82,19 @@ defmodule BlockScoutWeb.PagingHelper do
8182

8283
def token_transfers_types_options(_), do: [token_type: []]
8384

85+
@doc """
86+
Parse 'activity' query parameter from request option map for token transfers.
87+
Accepts: minting, burning, transfer, spawning (case-insensitive, comma-separated)
88+
"""
89+
@spec token_transfers_activity_options(map()) :: [{:activity, list}]
90+
def token_transfers_activity_options(%{"activity" => filters}) do
91+
[
92+
activity: filters_to_list(filters, @allowed_token_transfer_activity_labels)
93+
]
94+
end
95+
96+
def token_transfers_activity_options(_), do: [activity: []]
97+
8498
@doc """
8599
Parse 'type' query parameter from request option map
86100
"""

apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ defmodule BlockScoutWeb.API.V2.BlockView do
3333
priority_fee = block.base_fee_per_gas && BlockPriorityFeeCounter.fetch(block.hash)
3434

3535
transaction_fees = Block.transaction_fees(block.transactions)
36+
total_value = Block.total_value(block.transactions)
3637

3738
%{
3839
"height" => block.number,
@@ -59,6 +60,7 @@ defmodule BlockScoutWeb.API.V2.BlockView do
5960
"burnt_fees_percentage" => burnt_fees_percentage(burnt_fees, transaction_fees),
6061
"type" => block |> BlockView.block_type() |> String.downcase(),
6162
"transaction_fees" => transaction_fees,
63+
"total_value" => total_value,
6264
"withdrawals_count" => count_withdrawals(block)
6365
}
6466
|> chain_type_fields(block, single_block?)

apps/explorer/lib/explorer/chain.ex

Lines changed: 102 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -575,41 +575,42 @@ defmodule Explorer.Chain do
575575
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
576576
type_filter = Keyword.get(options, :type)
577577

578-
options
579-
|> Keyword.get(:paging_options, @default_paging_options)
580-
|> fetch_transactions_in_ascending_order_by_index()
581-
|> join(:inner, [transaction], block in assoc(transaction, :block))
582-
|> where([_, block], block.hash == ^block_hash)
583-
|> apply_filter_by_type_to_transactions(type_filter)
584-
|> join_associations(necessity_by_association)
585-
|> Transaction.put_has_token_transfers_to_transaction(old_ui?)
586-
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
587-
|> select_repo(options).all()
588-
|> (&if(old_ui?,
589-
do: &1,
590-
else:
591-
Enum.map(&1, fn transaction ->
592-
preload_token_transfers(transaction, @token_transfer_necessity_by_association, options)
593-
end)
594-
)).()
578+
transactions =
579+
options
580+
|> Keyword.get(:paging_options, @default_paging_options)
581+
|> fetch_transactions_in_ascending_order_by_index()
582+
|> join(:inner, [transaction], block in assoc(transaction, :block))
583+
|> where([_, block], block.hash == ^block_hash)
584+
|> apply_filter_by_type_to_transactions(type_filter)
585+
|> join_associations(necessity_by_association)
586+
|> Transaction.put_has_token_transfers_to_transaction(old_ui?)
587+
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
588+
|> select_repo(options).all()
589+
590+
# Use batch preloading instead of N+1 queries for new UI
591+
if old_ui? do
592+
transactions
593+
else
594+
batch_preload_token_transfers(transactions, @token_transfer_necessity_by_association, options)
595+
end
595596
end
596597

597598
@spec execution_node_to_transactions(Hash.Address.t(), [paging_options | necessity_by_association_option | api?()]) ::
598599
[Transaction.t()]
599600
def execution_node_to_transactions(execution_node_hash, options \\ []) when is_list(options) do
600601
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
601602

602-
options
603-
|> Keyword.get(:paging_options, @default_paging_options)
604-
|> fetch_transactions_in_descending_order_by_block_and_index()
605-
|> where(execution_node_hash: ^execution_node_hash)
606-
|> join_associations(necessity_by_association)
607-
|> Transaction.put_has_token_transfers_to_transaction(false)
608-
|> (& &1).()
609-
|> select_repo(options).all()
610-
|> (&Enum.map(&1, fn transaction ->
611-
preload_token_transfers(transaction, @token_transfer_necessity_by_association, options)
612-
end)).()
603+
transactions =
604+
options
605+
|> Keyword.get(:paging_options, @default_paging_options)
606+
|> fetch_transactions_in_descending_order_by_block_and_index()
607+
|> where(execution_node_hash: ^execution_node_hash)
608+
|> join_associations(necessity_by_association)
609+
|> Transaction.put_has_token_transfers_to_transaction(false)
610+
|> select_repo(options).all()
611+
612+
# Use batch preloading instead of N+1 queries
613+
batch_preload_token_transfers(transactions, @token_transfer_necessity_by_association, options)
613614
end
614615

615616
@spec block_to_withdrawals(
@@ -1368,6 +1369,62 @@ defmodule Explorer.Chain do
13681369

13691370
def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
13701371

1372+
@doc """
1373+
Batch preloads token transfers for multiple transactions in a single query.
1374+
This eliminates the N+1 query problem when loading token transfers for transaction lists.
1375+
1376+
For each transaction, loads up to `@token_transfers_per_transaction_preview` token transfers
1377+
with their associated addresses and tokens.
1378+
1379+
Token transfers are filtered by block_hash in Elixir to ensure data correctness during reorgs,
1380+
matching the original behavior where transfers are filtered by both transaction_hash AND block_hash.
1381+
"""
1382+
@spec batch_preload_token_transfers([Transaction.t()], map(), Keyword.t()) :: [Transaction.t()]
1383+
def batch_preload_token_transfers([], _necessity_by_association, _options), do: []
1384+
1385+
def batch_preload_token_transfers(transactions, necessity_by_association, options) do
1386+
# Build a map of transaction_hash -> block_hash for filtering
1387+
tx_block_hash_map =
1388+
transactions
1389+
|> Enum.map(fn tx -> {tx.hash, tx.block_hash} end)
1390+
|> Map.new()
1391+
1392+
transaction_hashes = Map.keys(tx_block_hash_map)
1393+
1394+
# Query all token transfers for all transactions in one query
1395+
# We fetch more than needed and limit per-transaction in Elixir
1396+
token_transfers =
1397+
from(tt in TokenTransfer,
1398+
where: tt.transaction_hash in ^transaction_hashes,
1399+
order_by: [asc: tt.transaction_hash, asc: tt.log_index]
1400+
)
1401+
|> join_associations(necessity_by_association)
1402+
|> select_repo(options).all()
1403+
|> flat_1155_batch_token_transfers()
1404+
# Filter by block_hash to ensure data correctness (matches original behavior)
1405+
# If tx.block_hash is nil (pending tx), accept any token transfer for that tx
1406+
# Otherwise, only accept transfers from the same block
1407+
|> Enum.filter(fn tt ->
1408+
tx_block_hash = Map.get(tx_block_hash_map, tt.transaction_hash)
1409+
is_nil(tx_block_hash) or tt.block_hash == tx_block_hash
1410+
end)
1411+
1412+
# Group token transfers by transaction hash
1413+
transfers_by_tx_hash =
1414+
token_transfers
1415+
|> Enum.group_by(& &1.transaction_hash)
1416+
1417+
# Attach token transfers to each transaction, limiting to preview count
1418+
Enum.map(transactions, fn tx ->
1419+
tx_transfers =
1420+
transfers_by_tx_hash
1421+
|> Map.get(tx.hash, [])
1422+
|> Enum.take(@token_transfers_per_transaction_preview)
1423+
1424+
%Transaction{tx | token_transfers: tx_transfers}
1425+
end)
1426+
end
1427+
13711428
@doc """
13721429
Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for
13731430
those `hashes`.
@@ -2685,22 +2742,23 @@ defmodule Explorer.Chain do
26852742
[]
26862743

26872744
_ ->
2688-
paging_options
2689-
|> Transaction.fetch_transactions()
2690-
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
2691-
|> apply_filter_by_method_id_to_transactions(method_id_filter)
2692-
|> apply_filter_by_type_to_transactions(type_filter)
2693-
|> join_associations(necessity_by_association)
2694-
|> Transaction.put_has_token_transfers_to_transaction(old_ui?)
2695-
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
2696-
|> select_repo(options).all()
2697-
|> (&if(old_ui?,
2698-
do: &1,
2699-
else:
2700-
Enum.map(&1, fn transaction ->
2701-
preload_token_transfers(transaction, @token_transfer_necessity_by_association, options)
2702-
end)
2703-
)).()
2745+
transactions =
2746+
paging_options
2747+
|> Transaction.fetch_transactions()
2748+
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
2749+
|> apply_filter_by_method_id_to_transactions(method_id_filter)
2750+
|> apply_filter_by_type_to_transactions(type_filter)
2751+
|> join_associations(necessity_by_association)
2752+
|> Transaction.put_has_token_transfers_to_transaction(old_ui?)
2753+
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
2754+
|> select_repo(options).all()
2755+
2756+
# Use batch preloading instead of N+1 queries for new UI
2757+
if old_ui? do
2758+
transactions
2759+
else
2760+
batch_preload_token_transfers(transactions, @token_transfer_necessity_by_association, options)
2761+
end
27042762
end
27052763
end
27062764

apps/explorer/lib/explorer/chain/block.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,21 @@ defmodule Explorer.Chain.Block do
358358
end)
359359
end
360360

361+
@doc """
362+
Calculates total value transferred for the list of transactions (from a single block)
363+
"""
364+
@spec total_value([Transaction.t()]) :: Decimal.t()
365+
def total_value(transactions) do
366+
Enum.reduce(transactions, Decimal.new(0), fn %{value: value}, acc ->
367+
if value do
368+
value.value
369+
|> Decimal.add(acc)
370+
else
371+
acc
372+
end
373+
end)
374+
end
375+
361376
@doc """
362377
Finds blob transaction gas price for the list of transactions (from a single block)
363378
"""

apps/explorer/lib/explorer/chain/token_transfer.ex

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ defmodule Explorer.Chain.TokenTransfer do
140140
import Ecto.Changeset
141141

142142
alias Explorer.{Chain, Helper}
143-
alias Explorer.Chain.{DenormalizationHelper, Hash, Log, TokenTransfer}
143+
alias Explorer.Chain.{DenormalizationHelper, Hash, Log, SmartContract, TokenTransfer}
144144
alias Explorer.Chain.SmartContract.Proxy.Models.Implementation
145145
alias Explorer.{PagingOptions, Repo}
146146

@@ -292,6 +292,7 @@ defmodule Explorer.Chain.TokenTransfer do
292292
def fetch(options) do
293293
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
294294
token_type = Keyword.get(options, :token_type)
295+
activity = Keyword.get(options, :activity, [])
295296

296297
case paging_options do
297298
%PagingOptions{key: {0, 0}} ->
@@ -310,6 +311,7 @@ defmodule Explorer.Chain.TokenTransfer do
310311
|> preload(^preloads)
311312
|> order_by([tt], desc: tt.block_number, desc: tt.log_index)
312313
|> maybe_filter_by_token_type(token_type)
314+
|> maybe_filter_by_activity(activity)
313315
|> page_token_transfer(paging_options)
314316
|> limit(^paging_options.page_size)
315317
|> Chain.select_repo(options).all()
@@ -331,6 +333,51 @@ defmodule Explorer.Chain.TokenTransfer do
331333
end
332334
end
333335

336+
@doc """
337+
Filters token transfers by activity type (minting, burning, transfer, spawning).
338+
Activity is determined by comparing from/to addresses with the burn address (0x0...0).
339+
"""
340+
defp maybe_filter_by_activity(query, activities) when is_list(activities) do
341+
if Enum.empty?(activities) do
342+
query
343+
else
344+
{:ok, burn_address_hash} = Chain.string_to_address_hash(SmartContract.burn_address_hash_string())
345+
346+
conditions =
347+
activities
348+
|> Enum.map(&activity_to_condition(&1, burn_address_hash))
349+
|> Enum.reject(&is_nil/1)
350+
351+
if Enum.empty?(conditions) do
352+
query
353+
else
354+
combined_condition = Enum.reduce(conditions, fn condition, acc ->
355+
dynamic([tt], ^acc or ^condition)
356+
end)
357+
358+
where(query, ^combined_condition)
359+
end
360+
end
361+
end
362+
363+
defp activity_to_condition("MINTING", burn_address_hash) do
364+
dynamic([tt], tt.from_address_hash == ^burn_address_hash and tt.to_address_hash != ^burn_address_hash)
365+
end
366+
367+
defp activity_to_condition("BURNING", burn_address_hash) do
368+
dynamic([tt], tt.to_address_hash == ^burn_address_hash and tt.from_address_hash != ^burn_address_hash)
369+
end
370+
371+
defp activity_to_condition("SPAWNING", burn_address_hash) do
372+
dynamic([tt], tt.from_address_hash == ^burn_address_hash and tt.to_address_hash == ^burn_address_hash)
373+
end
374+
375+
defp activity_to_condition("TRANSFER", burn_address_hash) do
376+
dynamic([tt], tt.from_address_hash != ^burn_address_hash and tt.to_address_hash != ^burn_address_hash)
377+
end
378+
379+
defp activity_to_condition(_, _burn_address_hash), do: nil
380+
334381
@spec count_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
335382
def count_token_transfers_from_token_hash(token_address_hash) do
336383
query =

0 commit comments

Comments
 (0)