Blockchain explorer for Ethereum based network and a tool for inspecting and analyzing EVM based blockchains.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
blockscout/apps/explorer/lib/explorer/etherscan/logs.ex

280 lines
8.8 KiB

defmodule Explorer.Etherscan.Logs do
@moduledoc """
This module contains functions for working with logs, as they pertain to the
`Explorer.Etherscan` context.
"""
import Ecto.Query, only: [from: 2, where: 3, subquery: 1, order_by: 3, union: 2]
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Block, InternalTransaction, Log, Transaction}
@base_filter %{
from_block: nil,
to_block: nil,
address_hash: nil,
first_topic: nil,
second_topic: nil,
third_topic: nil,
fourth_topic: nil,
topic0_1_opr: nil,
topic0_2_opr: nil,
topic0_3_opr: nil,
topic1_2_opr: nil,
topic1_3_opr: nil,
topic2_3_opr: nil
}
@log_fields [
:data,
:first_topic,
:second_topic,
:third_topic,
:fourth_topic,
:index,
:address_hash,
:transaction_hash,
:type
]
@default_paging_options %{block_number: nil, transaction_index: nil, log_index: nil}
@doc """
Gets a list of logs that meet the criteria in a given filter map.
Required filter parameters:
* `from_block`
* `to_block`
* `address_hash` and/or `{x}_topic`
* When multiple `{x}_topic` params are provided, then the corresponding
`topic{x}_{x}_opr` param is required. For example, if "first_topic" and
"second_topic" are provided, then "topic0_1_opr" is required.
Supported `{x}_topic`s:
* first_topic
* second_topic
* third_topic
* fourth_topic
Supported `topic{x}_{x}_opr`s:
* topic0_1_opr
* topic0_2_opr
* topic0_3_opr
* topic1_2_opr
* topic1_3_opr
* topic2_3_opr
"""
@spec list_logs(map()) :: [map()]
def list_logs(filter, paging_options \\ @default_paging_options)
def list_logs(%{address_hash: address_hash} = filter, paging_options) when not is_nil(address_hash) do
paging_options = if is_nil(paging_options), do: @default_paging_options, else: paging_options
prepared_filter = Map.merge(@base_filter, filter)
logs_query = where_topic_match(Log, prepared_filter)
query_to_address_hash_wrapped =
logs_query
|> internal_transaction_query(:to_address_hash, prepared_filter, address_hash)
|> Chain.wrapped_union_subquery()
query_from_address_hash_wrapped =
logs_query
|> internal_transaction_query(:from_address_hash, prepared_filter, address_hash)
|> Chain.wrapped_union_subquery()
query_created_contract_address_hash_wrapped =
logs_query
|> internal_transaction_query(:created_contract_address_hash, prepared_filter, address_hash)
|> Chain.wrapped_union_subquery()
internal_transaction_log_query =
query_to_address_hash_wrapped
|> union(^query_from_address_hash_wrapped)
|> union(^query_created_contract_address_hash_wrapped)
all_transaction_logs_query =
from(transaction in Transaction,
join: log in ^logs_query,
on: log.transaction_hash == transaction.hash,
where: transaction.block_number >= ^prepared_filter.from_block,
where: transaction.block_number <= ^prepared_filter.to_block,
where:
transaction.to_address_hash == ^address_hash or
transaction.from_address_hash == ^address_hash or
transaction.created_contract_address_hash == ^address_hash,
select: map(log, ^@log_fields),
select_merge: map(transaction, [:gas_price, :gas_used, :block_number]),
select_merge: %{
transaction_index: transaction.index
},
union: ^internal_transaction_log_query
)
query_with_blocks =
from(log_transaction_data in subquery(all_transaction_logs_query),
join: block in Block,
on: block.number == log_transaction_data.block_number,
where: log_transaction_data.address_hash == ^address_hash,
order_by: block.number,
limit: 1000,
select_merge: %{
block_timestamp: block.timestamp,
block_consensus: block.consensus,
block_hash: block.hash
}
)
query_with_consensus =
if Map.get(filter, :allow_non_consensus) do
query_with_blocks
else
from([_, block] in query_with_blocks,
where: block.consensus == true
)
end
query_with_consensus
|> order_by([log], asc: log.index)
|> page_logs(paging_options)
|> Repo.all()
end
# Since address_hash was not present, we know that a
# topic filter has been applied, so we use a different
# query that is optimized for a logs filter over an
# address_hash
def list_logs(filter, paging_options) do
paging_options = if is_nil(paging_options), do: @default_paging_options, else: paging_options
prepared_filter = Map.merge(@base_filter, filter)
logs_query = where_topic_match(Log, prepared_filter)
block_transaction_query =
from(transaction in Transaction,
join: block in assoc(transaction, :block),
where: block.number >= ^prepared_filter.from_block,
where: block.number <= ^prepared_filter.to_block,
select: %{
transaction_hash: transaction.hash,
gas_price: transaction.gas_price,
gas_used: transaction.gas_used,
transaction_index: transaction.index,
block_hash: block.hash,
block_number: block.number,
block_timestamp: block.timestamp,
block_consensus: block.consensus
}
)
query_with_consensus =
if Map.get(filter, :allow_non_consensus) do
block_transaction_query
else
from([_, block] in block_transaction_query,
where: block.consensus == true
)
end
query_with_block_transaction_data =
from(log in logs_query,
join: block_transaction_data in subquery(query_with_consensus),
on: block_transaction_data.transaction_hash == log.transaction_hash,
order_by: block_transaction_data.block_number,
limit: 1000,
select: block_transaction_data,
select_merge: map(log, ^@log_fields)
)
query_with_block_transaction_data
|> order_by([log], asc: log.index)
|> page_logs(paging_options)
|> Repo.all()
end
@topics [
:first_topic,
:second_topic,
:third_topic,
:fourth_topic
]
@topic_operations %{
topic0_1_opr: {:first_topic, :second_topic},
topic0_2_opr: {:first_topic, :third_topic},
topic0_3_opr: {:first_topic, :fourth_topic},
topic1_2_opr: {:second_topic, :third_topic},
topic1_3_opr: {:second_topic, :fourth_topic},
topic2_3_opr: {:third_topic, :fourth_topic}
}
defp where_topic_match(query, filter) do
case Enum.filter(@topics, &filter[&1]) do
[] ->
query
[topic] ->
where(query, [l], field(l, ^topic) in ^List.wrap(filter[topic]))
_ ->
where_multiple_topics_match(query, filter)
end
end
defp where_multiple_topics_match(query, filter) do
Enum.reduce(Map.keys(@topic_operations), query, fn topic_operation, acc_query ->
where_multiple_topics_match(acc_query, filter, topic_operation, filter[topic_operation])
end)
end
defp where_multiple_topics_match(query, filter, topic_operation, "and") do
{topic_a, topic_b} = @topic_operations[topic_operation]
where(query, [l], field(l, ^topic_a) == ^filter[topic_a] and field(l, ^topic_b) in ^List.wrap(filter[topic_b]))
end
defp where_multiple_topics_match(query, filter, topic_operation, "or") do
{topic_a, topic_b} = @topic_operations[topic_operation]
where(query, [l], field(l, ^topic_a) == ^filter[topic_a] or field(l, ^topic_b) in ^List.wrap(filter[topic_b]))
end
defp where_multiple_topics_match(query, _, _, _), do: query
defp page_logs(query, %{block_number: nil, transaction_index: nil, log_index: nil}) do
query
end
defp page_logs(query, %{block_number: block_number, transaction_index: transaction_index, log_index: log_index}) do
from(
data in query,
where:
data.index > ^log_index and data.block_number >= ^block_number and
data.transaction_index >= ^transaction_index
)
end
defp internal_transaction_query(logs_query, direction, prepared_filter, address_hash) do
query =
from(internal_transaction in InternalTransaction.where_nonpending_block(),
join: transaction in assoc(internal_transaction, :transaction),
join: log in ^logs_query,
on: log.transaction_hash == internal_transaction.transaction_hash,
where: internal_transaction.block_number >= ^prepared_filter.from_block,
where: internal_transaction.block_number <= ^prepared_filter.to_block,
select:
merge(map(log, ^@log_fields), %{
gas_price: transaction.gas_price,
gas_used: transaction.gas_used,
transaction_index: transaction.index,
block_number: transaction.block_number
})
)
query
|> InternalTransaction.where_address_fields_match(address_hash, direction)
end
end