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/chain.ex

651 lines
21 KiB

defmodule Explorer.Chain do
@moduledoc """
The chain context.
"""
import Ecto.Query, only: [from: 2, or_where: 3, order_by: 2, preload: 2, where: 2]
alias Explorer.Chain.{
Address,
Block,
BlockTransaction,
InternalTransaction,
Log,
Receipt,
Transaction,
Wei
}
alias Explorer.Repo.NewRelic, as: Repo
# Types
@typedoc """
The name of an association on the `t:Ecto.Schema.t/0`
"""
@type association :: atom()
@typedoc """
* `:optional` - the association is optional and only needs to be loaded if available
* `:required` - the association is required and MUST be loaded. If it is not available, then the parent struct
SHOULD NOT be returned.
"""
@type necessity :: :optional | :required
@typedoc """
The `t:necessity/0` of each association that should be loaded
"""
@type necessity_by_association :: %{association => necessity}
@typedoc """
Pagination params used by `scrivener`
"""
@type pagination :: map()
@typep direction_option :: :to | :from
@typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association}
@typep pagination_option :: {:pagination, pagination}
# Functions
@doc """
`t:Explorer.Chain.Transaction/0`s from `address`.
## Options
* `:direction` - if specified, will filter transactions by address type. If `:to` is specified, only transactions
where the "to" address matches will be returned. Likewise, if `:from` is specified, only transactions where the
"from" address matches will be returned. If :direction is omitted, transactions either to or from the address
will be returned.
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`.
* `:pagination` - pagination params to pass to scrivener.
"""
@spec address_to_transactions(Address.t(), [
direction_option | necessity_by_association_option | pagination_option
]) :: %Scrivener.Page{entries: [Transaction.t()]}
def address_to_transactions(address = %Address{}, options \\ [])
when is_list(options) do
address_id_to_transactions(address.id, options)
end
@doc """
The `t:Explorer.Chain.Address.t/0` `balance` in `unit`.
"""
@spec balance(Address.t(), :wei) :: Wei.t() | nil
@spec balance(Address.t(), :gwei) :: Wei.gwei() | nil
@spec balance(Address.t(), :ether) :: Wei.ether() | nil
def balance(%Address{balance: balance}, unit) do
case balance do
nil -> nil
_ -> Wei.to(balance, unit)
end
end
@doc """
Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`.
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`.
* `:pagination` - pagination params to pass to scrivener.
"""
@spec block_to_transactions(Block.t()) :: %Scrivener.Page{entries: [Transaction.t()]}
@spec block_to_transactions(Block.t(), [necessity_by_association_option | pagination_option]) :: %Scrivener.Page{
entries: [Transaction.t()]
}
def block_to_transactions(%Block{id: block_id}, options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
pagination = Keyword.get(options, :pagination, %{})
query =
from(
transaction in Transaction,
inner_join: block in assoc(transaction, :block),
where: block.id == ^block_id,
order_by: [desc: transaction.inserted_at]
)
query
|> join_associations(necessity_by_association)
|> Repo.paginate(pagination)
end
@doc """
Counts the number of `t:Explorer.Chain.Transaction.t/0` in the `block`.
"""
@spec block_to_transaction_count(Block.t()) :: non_neg_integer()
def block_to_transaction_count(%Block{id: block_id}) do
query =
from(
block_transaction in BlockTransaction,
join: block in assoc(block_transaction, :block),
where: block_transaction.block_id == ^block_id
)
Repo.aggregate(query, :count, :block_id)
end
@doc """
How many blocks have confirmed `block` based on the current `max_block_number`
"""
@spec confirmations(Block.t(), [{:max_block_number, Block.block_number()}]) :: non_neg_integer()
def confirmations(%Block{number: number}, named_arguments) when is_list(named_arguments) do
max_block_number = Keyword.fetch!(named_arguments, :max_block_number)
max_block_number - number
end
@doc """
Creates an address.
## Examples
iex> Explorer.Addresses.create_address(%{field: value})
{:ok, %Address{}}
iex> Explorer.Addresses.create_address(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec create_address(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def create_address(attrs \\ %{}) do
%Address{}
|> Address.changeset(attrs)
|> Repo.insert()
end
@doc """
Ensures that an `t:Explorer.Address.t/0` exists with the given `hash`.
If a `t:Explorer.Address.t/0` with `hash` already exists, it is returned
iex> Explorer.Addresses.ensure_hash_address(existing_hash)
{:ok, %Address{}}
If a `t:Explorer.Address.t/0` does not exist with `hash`, it is created and returned
iex> Explorer.Addresses.ensure_hash_address(new_hash)
{:ok, %Address{}}
There is a chance of a race condition when interacting with the database: the `t:Explorer.Address.t/0` may not exist
when first checked, then already exist when it is tried to be created because another connection creates the addres,
then another process deletes the address after this process's connection see it was created, but before it can be
retrieved. In scenario, the address may be not found as only one retry is attempted to prevent infinite loops.
iex> Explorer.Addresses.ensure_hash_address(flicker_hash)
{:error, :not_found}
"""
@spec ensure_hash_address(Address.hash()) :: {:ok, Address.t()} | {:error, :not_found}
def ensure_hash_address(hash) when is_binary(hash) do
with {:error, :not_found} <- hash_to_address(hash),
{:error, _} <- create_address(%{hash: hash}) do
# assume race condition occurred and someone else created the address between the first
# hash_to_address and create_address
hash_to_address(hash)
end
end
@doc """
The fee a `transaction` paid for the `t:Explorer.Transaction.t/0` `gas`
If the transaction is pending, then the fee will be a range of `unit`
iex> Explorer.Chain.fee(
...> %Explorer.Chain.Transaction{gas: Decimal.new(3), gas_price: Decimal.new(2), receipt: nil},
...> :wei
...> )
{:maximum, Decimal.new(6)}
If the transaction has been confirmed in block, then the fee will be the actual fee paid in `unit` for the `gas_used`
in the `receipt`.
iex> Explorer.Chain.fee(
...> %Explorer.Chain.Transaction{
...> gas: Decimal.new(3),
...> gas_price: Decimal.new(2),
...> receipt: Explorer.Chain.Receipt{gas_used: Decimal.new(2)}
...> },
...> :wei
...> )
{:actual, Decimal.new(4)}
"""
@spec fee(%Transaction{receipt: nil}, :ether | :gwei | :wei) :: {:maximum, Decimal.t()}
def fee(%Transaction{gas: gas, gas_price: gas_price, receipt: nil}, unit) do
fee =
gas
|> Decimal.mult(gas_price)
|> Wei.to(unit)
{:maximum, fee}
end
@spec fee(%Transaction{receipt: Receipt.t()}, :ether | :gwei | :wei) :: {:actual, Decimal.t()}
def fee(%Transaction{gas_price: gas_price, receipt: %Receipt{gas_used: gas_used}}, unit) do
fee =
gas_used
|> Decimal.mult(gas_price)
|> Wei.to(unit)
{:actual, fee}
end
@doc """
The `t:Explorer.Chain.Transaction.t/0` `gas_price` of the `transaction` in `unit`.
"""
@spec gas_price(Transaction.t(), :wei) :: Wei.t()
@spec gas_price(Transaction.t(), :gwei) :: Wei.gwei()
@spec gas_price(Transaction.t(), :ether) :: Wei.ether()
def gas_price(%Transaction{gas_price: gas_price}, unit) do
Wei.to(gas_price, unit)
end
@doc """
Converts `t:Explorer.Chain.Address.t/0` `hash` to the `t:Explorer.Chain.Address.t/0` with that `hash`.
Returns `{:ok, %Explorer.Chain.Address{}}` if found
iex> hash_to_address("0x0addressaddressaddressaddressaddressaddr")
{:ok, %Explorer.Chain.Address{}}
Returns `{:error, :not_found}` if not found
iex> hash_to_address("0x1addressaddressaddressaddressaddressaddr")
{:error, :not_found}
"""
@spec hash_to_address(Address.hash()) :: {:ok, Address.t()} | {:error, :not_found}
def hash_to_address(hash) do
Address
|> where_hash(hash)
|> preload([:credit, :debit])
|> Repo.one()
|> case do
nil -> {:error, :not_found}
address -> {:ok, address}
end
end
@doc """
Converts `t:Explorer.Chain.Transaction.t/0` `hash` to the `t:Explorer.Chain.Transaction.t/0` with that `hash`.
Returns `{:ok, %Explorer.Chain.Transaction{}}` if found
iex> hash_to_transaction("0x0addressaddressaddressaddressaddressaddr")
{:ok, %Explorer.Chain.Transaction{}}
Returns `{:error, :not_found}` if not found
iex> hash_to_transaction("0x1addressaddressaddressaddressaddressaddr")
{:error, :not_found}
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`.
"""
@spec hash_to_transaction(Transaction.hash(), [necessity_by_association_option]) ::
{:ok, Transaction.t()} | {:error, :not_found}
def hash_to_transaction(hash, options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
Transaction
|> join_associations(necessity_by_association)
|> where_hash(hash)
|> Repo.one()
|> case do
nil -> {:error, :not_found}
transaction -> {:ok, transaction}
end
end
@doc """
Converts `t:Explorer.Address.t/0` `id` to the `t:Explorer.Address.t/0` with that `id`.
Returns `{:ok, %Explorer.Address{}}` if found
iex> id_to_address(123)
{:ok, %Address{}}
Returns `{:error, :not_found}` if not found
iex> id_to_address(456)
{:error, :not_found}
"""
@spec id_to_address(id :: non_neg_integer()) :: {:ok, Address.t()} | {:error, :not_found}
def id_to_address(id) do
Address
|> Repo.get(id)
|> case do
nil ->
{:error, :not_found}
address ->
{:ok, Repo.preload(address, [:credit, :debit])}
end
end
@doc """
The last `t:Explorer.Chain.Transaction.t/0` `id`.
"""
@spec last_transaction_id([{:pending, boolean()}]) :: non_neg_integer()
def last_transaction_id(options \\ []) when is_list(options) do
query =
from(
t in Transaction,
select: t.id,
order_by: [desc: t.id],
limit: 1
)
query
|> where_pending(options)
|> Repo.one()
|> Kernel.||(0)
end
@doc """
Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`.
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`.
* `:pagination` - pagination params to pass to scrivener.
"""
@spec list_blocks([necessity_by_association_option | pagination_option]) :: %Scrivener.Page{
entries: [Block.t()]
}
def list_blocks(options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
pagination = Keyword.get(options, :pagination, %{})
Block
|> join_associations(necessity_by_association)
|> order_by(desc: :number)
|> Repo.paginate(pagination)
end
@doc """
The maximum `t:Explorer.Chain.Block.t/0` `number`
"""
@spec max_block_number() :: Block.block_number()
def max_block_number do
Repo.aggregate(Block, :max, :number)
end
@doc """
Finds `t:Explorer.Chain.Block.t/0` with `number`
"""
@spec number_to_block(Block.block_number()) :: {:ok, Block.t()} | {:error, :not_found}
def number_to_block(number) do
Block
|> where(number: ^number)
|> Repo.one()
|> case do
nil -> {:error, :not_found}
block -> {:ok, block}
end
end
@doc """
Count of `t:Explorer.Chain.Transaction.t/0`.
## Options
* `:pending`
* `true` - only count pending transactions
* `false` - count all transactions
"""
@spec transaction_count([{:pending, boolean()}]) :: non_neg_integer()
def transaction_count(options \\ []) when is_list(options) do
Transaction
|> where_pending(options)
|> Repo.aggregate(:count, :id)
end
@doc """
`t:Explorer.Chain.InternalTransaction/0`s in `t:Explorer.Chain.Transaction.t/0` with `hash`
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association,
then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the list.
"""
@spec transaction_hash_to_internal_transactions(Transaction.hash()) :: [InternalTransaction.t()]
def transaction_hash_to_internal_transactions(hash, options \\ [])
when is_binary(hash) and is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
InternalTransaction
|> for_parent_transaction(hash)
|> join_associations(necessity_by_association)
|> Repo.all()
end
@doc """
Returns the list of transactions that occurred recently (10) before `t:Explorer.Chain.Transaction.t/0` `id`.
## Examples
iex> Explorer.Chain.list_transactions_before_id(id)
[%Explorer.Chain.Transaction{}, ...]
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association,
then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the list.
"""
@spec transactions_recently_before_id(id :: non_neg_integer, [necessity_by_association_option]) :: [
Transaction.t()
]
def transactions_recently_before_id(id, options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
Transaction
|> join_associations(necessity_by_association)
|> recently_before_id(id)
|> where_pending(options)
|> Repo.all()
end
@doc """
Finds all `t:Explorer.Chain.Log.t/0`s for `t:Explorer.Chain.Transaction.t/0`.
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Log.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Log.t/0` will not be included in the page `entries`.
* `:pagination` - pagination params to pass to scrivener.
"""
@spec transaction_to_logs(Transaction.t(), [
necessity_by_association_option | pagination_option
]) :: %Scrivener.Page{entries: [Log.t()]}
def transaction_to_logs(%Transaction{hash: hash}, options \\ []) when is_list(options) do
transaction_hash_to_logs(hash, options)
end
@doc """
Converts `transaction` with its `receipt` loaded to the status of the `t:Explorer.Chain.Transaction.t/0`.
## Returns
* `:failed` - the transaction failed without running out of gas
* `:pending` - the transaction has not be confirmed in a block yet
* `:out_of_gas` - the transaction failed because it ran out of gas
* `:success` - the transaction has been confirmed in a block
"""
@spec transaction_to_status(Transaction.t()) :: :failed | :pending | :out_of_gas | :success
def transaction_to_status(%Transaction{receipt: nil}), do: :pending
def transaction_to_status(%Transaction{receipt: %Receipt{status: 1}}), do: :success
def transaction_to_status(%Transaction{
gas: gas,
receipt: %Receipt{gas_used: gas_used, status: 0}
})
when gas_used >= gas do
:out_of_gas
end
def transaction_to_status(%Transaction{receipt: %Receipt{status: 0}}), do: :failed
@doc """
Updates `balance` of `t:Explorer.Address.t/0` with `hash`.
If `t:Explorer.Address.t/0` with `hash` does not already exist, it is created first.
"""
@spec update_balance(Address.hash(), Address.balance()) ::
{:ok, Address.t()} | {:error, Ecto.Changeset.t()} | {:error, reason :: term}
def update_balance(hash, balance) when is_binary(hash) do
changes = %{
balance: balance
}
with {:ok, address} <- ensure_hash_address(hash) do
address
|> Address.balance_changeset(changes)
|> Repo.update()
end
end
@doc """
The `t:Explorer.Chain.Transaction.t/0` or `t:Explorer.Chain.InternalTransaction.t/0` `value` of the `transaction` in
`unit`.
"""
@spec value(InternalTransaction.t(), :wei) :: Wei.t()
@spec value(InternalTransaction.t(), :gwei) :: Wei.gwei()
@spec value(InternalTransaction.t(), :ether) :: Wei.ether()
@spec value(Transaction.t(), :wei) :: Wei.t()
@spec value(Transaction.t(), :gwei) :: Wei.gwei()
@spec value(Transaction.t(), :ether) :: Wei.ether()
def value(%type{value: value}, unit) when type in [InternalTransaction, Transaction] do
Wei.to(value, unit)
end
## Private Functions
defp address_id_to_transactions(address_id, named_arguments)
when is_integer(address_id) and is_list(named_arguments) do
address_fields =
case Keyword.get(named_arguments, :direction) do
:to -> [:to_address_id]
:from -> [:from_address_id]
nil -> [:to_address_id, :from_address_id]
end
necessity_by_association = Keyword.get(named_arguments, :necessity_by_association, %{})
pagination = Keyword.get(named_arguments, :pagination, %{})
Transaction
|> join_associations(necessity_by_association)
|> reverse_chronologically()
|> where_address_fields_match(address_fields, address_id)
|> Repo.paginate(pagination)
end
defp for_parent_transaction(query, hash) when is_binary(hash) do
from(
child in query,
inner_join: transaction in assoc(child, :transaction),
where: fragment("lower(?)", transaction.hash) == ^String.downcase(hash)
)
end
defp join_association(query, association, necessity) when is_atom(association) do
case necessity do
:optional ->
preload(query, ^association)
:required ->
from(q in query, inner_join: a in assoc(q, ^association), preload: [{^association, a}])
end
end
defp join_associations(query, necessity_by_association) when is_map(necessity_by_association) do
Enum.reduce(necessity_by_association, query, fn {association, join}, acc_query ->
join_association(acc_query, association, join)
end)
end
defp recently_before_id(query, id) do
from(
q in query,
where: q.id < ^id,
order_by: [desc: q.id],
limit: 10
)
end
defp reverse_chronologically(query) do
from(q in query, order_by: [desc: q.inserted_at, desc: q.id])
end
defp transaction_hash_to_logs(transaction_hash, options)
when is_binary(transaction_hash) and is_list(options) do
lower_transaction_hash = String.downcase(transaction_hash)
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
pagination = Keyword.get(options, :pagination, %{})
query =
from(
log in Log,
join: transaction in assoc(log, :transaction),
where: fragment("lower(?)", transaction.hash) == ^lower_transaction_hash,
order_by: [asc: :index]
)
query
|> join_associations(necessity_by_association)
|> Repo.paginate(pagination)
end
defp where_address_fields_match(query, address_fields, address_id) do
Enum.reduce(address_fields, query, fn field, query ->
or_where(query, [t], field(t, ^field) == ^address_id)
end)
end
defp where_hash(query, hash) do
from(
q in query,
where: fragment("lower(?)", q.hash) == ^String.downcase(hash)
)
end
defp where_pending(query, options) when is_list(options) do
pending = Keyword.get(options, :pending, false)
where_pending(query, pending)
end
defp where_pending(query, false), do: query
defp where_pending(query, true) do
from(
transaction in query,
where:
fragment(
"NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)",
transaction.id
)
)
end
end