@@ -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
0 commit comments