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.
6345 lines
206 KiB
6345 lines
206 KiB
defmodule Explorer.Chain do
|
|
@moduledoc """
|
|
The chain context.
|
|
"""
|
|
|
|
import Ecto.Query,
|
|
only: [
|
|
dynamic: 1,
|
|
dynamic: 2,
|
|
from: 2,
|
|
join: 4,
|
|
join: 5,
|
|
limit: 2,
|
|
lock: 2,
|
|
offset: 2,
|
|
order_by: 2,
|
|
order_by: 3,
|
|
preload: 2,
|
|
preload: 3,
|
|
select: 2,
|
|
select: 3,
|
|
subquery: 1,
|
|
union: 2,
|
|
where: 2,
|
|
where: 3
|
|
]
|
|
|
|
import EthereumJSONRPC, only: [integer_to_quantity: 1, fetch_block_internal_transactions: 2]
|
|
import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0]
|
|
|
|
require Logger
|
|
|
|
alias ABI.TypeDecoder
|
|
alias Ecto.{Changeset, Multi}
|
|
|
|
alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction
|
|
|
|
alias Explorer.Account.WatchlistAddress
|
|
|
|
alias Explorer.Counters.{LastFetchedCounter, TokenHoldersCounter, TokenTransfersCounter}
|
|
|
|
alias Explorer.Chain
|
|
|
|
alias Explorer.Chain.{
|
|
Address,
|
|
Address.CoinBalance,
|
|
Address.CoinBalanceDaily,
|
|
Address.CurrentTokenBalance,
|
|
Address.TokenBalance,
|
|
Block,
|
|
CurrencyHelper,
|
|
Data,
|
|
DecompiledSmartContract,
|
|
Hash,
|
|
Import,
|
|
InternalTransaction,
|
|
Log,
|
|
PendingBlockOperation,
|
|
Search,
|
|
SmartContract,
|
|
SmartContractAdditionalSource,
|
|
Token,
|
|
Token.Instance,
|
|
TokenTransfer,
|
|
Transaction,
|
|
Wei,
|
|
Withdrawal
|
|
}
|
|
|
|
alias Explorer.Chain.Block.{EmissionReward, Reward}
|
|
|
|
alias Explorer.Chain.Cache.{
|
|
Accounts,
|
|
BlockNumber,
|
|
Blocks,
|
|
ContractsCounter,
|
|
NewContractsCounter,
|
|
NewVerifiedContractsCounter,
|
|
Transactions,
|
|
Uncles,
|
|
VerifiedContractsCounter,
|
|
WithdrawalsSum
|
|
}
|
|
|
|
alias Explorer.Chain.Cache.Block, as: BlockCache
|
|
alias Explorer.Chain.Cache.PendingBlockOperation, as: PendingBlockOperationCache
|
|
alias Explorer.Chain.Fetcher.{CheckBytecodeMatchingOnDemand, LookUpSmartContractSourcesOnDemand}
|
|
alias Explorer.Chain.Import.Runner
|
|
alias Explorer.Chain.InternalTransaction.{CallType, Type}
|
|
|
|
alias Explorer.Market.MarketHistoryCache
|
|
alias Explorer.{PagingOptions, Repo}
|
|
alias Explorer.SmartContract.Helper
|
|
alias Explorer.SmartContract.Solidity.Verifier
|
|
|
|
alias Dataloader.Ecto, as: DataloaderEcto
|
|
|
|
@default_paging_options %PagingOptions{page_size: 50}
|
|
|
|
@token_transfers_per_transaction_preview 10
|
|
@token_transfers_necessity_by_association %{
|
|
[from_address: :smart_contract] => :optional,
|
|
[to_address: :smart_contract] => :optional,
|
|
[from_address: :names] => :optional,
|
|
[to_address: :names] => :optional,
|
|
token: :optional
|
|
}
|
|
|
|
@method_name_to_id_map %{
|
|
"approve" => "095ea7b3",
|
|
"transfer" => "a9059cbb",
|
|
"multicall" => "5ae401dc",
|
|
"mint" => "40c10f19",
|
|
"commit" => "f14fcbc8"
|
|
}
|
|
|
|
@revert_msg_prefix_1 "Revert: "
|
|
@revert_msg_prefix_2 "revert: "
|
|
@revert_msg_prefix_3 "reverted "
|
|
@revert_msg_prefix_4 "Reverted "
|
|
# Geth-like node
|
|
@revert_msg_prefix_5 "execution reverted: "
|
|
# keccak256("Error(string)")
|
|
@revert_error_method_id "08c379a0"
|
|
|
|
@limit_showing_transactions 10_000
|
|
@default_page_size 50
|
|
|
|
@typedoc """
|
|
The name of an association on the `t:Ecto.Schema.t/0`
|
|
"""
|
|
@type association :: atom()
|
|
|
|
@typedoc """
|
|
The max `t:Explorer.Chain.Block.block_number/0` for `consensus` `true` `t:Explorer.Chain.Block.t/0`s.
|
|
"""
|
|
@type block_height :: Block.block_number()
|
|
|
|
@typedoc """
|
|
Event type where data is broadcasted whenever data is inserted from chain indexing.
|
|
"""
|
|
@type chain_event ::
|
|
:addresses
|
|
| :address_coin_balances
|
|
| :blocks
|
|
| :block_rewards
|
|
| :exchange_rate
|
|
| :internal_transactions
|
|
| :logs
|
|
| :transactions
|
|
| :token_transfers
|
|
|
|
@type direction :: :from | :to
|
|
|
|
@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}
|
|
|
|
@typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association}
|
|
@typep paging_options :: {:paging_options, PagingOptions.t()}
|
|
@typep balance_by_day :: %{date: String.t(), value: Wei.t()}
|
|
@type api? :: {:api?, true | false}
|
|
|
|
@doc """
|
|
`t:Explorer.Chain.InternalTransaction/0`s from the address with the given `hash`.
|
|
|
|
This function excludes any internal transactions in the results where the
|
|
internal transaction has no siblings within the parent transaction.
|
|
|
|
## Options
|
|
|
|
* `:direction` - if specified, will filter internal transactions by address type. If `:to` is specified, only
|
|
internal transactions where the "to" address matches will be returned. Likewise, if `:from` is specified, only
|
|
internal transactions where the "from" address matches will be returned. If `:direction` is omitted, internal
|
|
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.InternalTransaction.t/0` has no associated record for that association,
|
|
then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the page `entries`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{block_number, transaction_index, index}`) and. Results will be the internal
|
|
transactions older than the `block_number`, `transaction index`, and `index` that are passed.
|
|
|
|
"""
|
|
@spec address_to_internal_transactions(Hash.Address.t(), [paging_options | necessity_by_association_option]) :: [
|
|
InternalTransaction.t()
|
|
]
|
|
def address_to_internal_transactions(hash, options \\ []) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
direction = Keyword.get(options, :direction)
|
|
|
|
from_block = from_block(options)
|
|
to_block = to_block(options)
|
|
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
if direction == nil || direction == "" do
|
|
query_to_address_hash_wrapped =
|
|
InternalTransaction
|
|
|> InternalTransaction.where_nonpending_block()
|
|
|> InternalTransaction.where_address_fields_match(hash, :to_address_hash)
|
|
|> InternalTransaction.where_block_number_in_period(from_block, to_block)
|
|
|> common_where_limit_order(paging_options)
|
|
|> wrapped_union_subquery()
|
|
|
|
query_from_address_hash_wrapped =
|
|
InternalTransaction
|
|
|> InternalTransaction.where_nonpending_block()
|
|
|> InternalTransaction.where_address_fields_match(hash, :from_address_hash)
|
|
|> InternalTransaction.where_block_number_in_period(from_block, to_block)
|
|
|> common_where_limit_order(paging_options)
|
|
|> wrapped_union_subquery()
|
|
|
|
query_created_contract_address_hash_wrapped =
|
|
InternalTransaction
|
|
|> InternalTransaction.where_nonpending_block()
|
|
|> InternalTransaction.where_address_fields_match(hash, :created_contract_address_hash)
|
|
|> InternalTransaction.where_block_number_in_period(from_block, to_block)
|
|
|> common_where_limit_order(paging_options)
|
|
|> wrapped_union_subquery()
|
|
|
|
query_to_address_hash_wrapped
|
|
|> union(^query_from_address_hash_wrapped)
|
|
|> union(^query_created_contract_address_hash_wrapped)
|
|
|> wrapped_union_subquery()
|
|
|> common_where_limit_order(paging_options)
|
|
|> preload(:block)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).all()
|
|
else
|
|
InternalTransaction
|
|
|> InternalTransaction.where_nonpending_block()
|
|
|> InternalTransaction.where_address_fields_match(hash, direction)
|
|
|> InternalTransaction.where_block_number_in_period(from_block, to_block)
|
|
|> common_where_limit_order(paging_options)
|
|
|> preload(:block)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).all()
|
|
end
|
|
end
|
|
|
|
def wrapped_union_subquery(query) do
|
|
from(
|
|
q in subquery(query),
|
|
select: q
|
|
)
|
|
end
|
|
|
|
defp common_where_limit_order(query, paging_options) do
|
|
query
|
|
|> InternalTransaction.where_is_different_from_parent_transaction()
|
|
|> page_internal_transaction(paging_options, %{index_int_tx_desc_order: true})
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by(
|
|
[it],
|
|
desc: it.block_number,
|
|
desc: it.transaction_index,
|
|
desc: it.index
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Fetches the transactions related to the address with the given hash, including
|
|
transactions that only have the address in the `token_transfers` related table
|
|
and rewards for block validation.
|
|
|
|
This query is divided into multiple subqueries intentionally in order to
|
|
improve the listing performance.
|
|
|
|
The `token_transfers` table tends to grow exponentially, and the query results
|
|
with a `transactions` `join` statement takes too long.
|
|
|
|
To solve this the `transaction_hashes` are fetched in a separate query, and
|
|
paginated through the `block_number` already present in the `token_transfers`
|
|
table.
|
|
|
|
## 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`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{block_number, index}`) and. Results will be the transactions older than
|
|
the `block_number` and `index` that are passed.
|
|
|
|
"""
|
|
@spec address_to_transactions_with_rewards(Hash.Address.t(), [paging_options | necessity_by_association_option]) ::
|
|
[
|
|
Transaction.t()
|
|
]
|
|
def address_to_transactions_with_rewards(address_hash, options \\ []) when is_list(options) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do
|
|
cond do
|
|
Keyword.get(options, :direction) == :from ->
|
|
address_to_transactions_without_rewards(address_hash, options)
|
|
|
|
address_has_rewards?(address_hash) ->
|
|
address_with_rewards(address_hash, options, paging_options)
|
|
|
|
true ->
|
|
address_to_transactions_without_rewards(address_hash, options)
|
|
end
|
|
else
|
|
address_to_transactions_without_rewards(address_hash, options)
|
|
end
|
|
end
|
|
|
|
defp address_with_rewards(address_hash, options, paging_options) do
|
|
%{payout_key: block_miner_payout_address} = Reward.get_validator_payout_key_by_mining_from_db(address_hash, options)
|
|
|
|
if block_miner_payout_address && address_hash == block_miner_payout_address do
|
|
transactions_with_rewards_results(address_hash, options, paging_options)
|
|
else
|
|
address_to_transactions_without_rewards(address_hash, options)
|
|
end
|
|
end
|
|
|
|
defp transactions_with_rewards_results(address_hash, options, paging_options) do
|
|
blocks_range = address_to_transactions_tasks_range_of_blocks(address_hash, options)
|
|
|
|
rewards_task =
|
|
Task.async(fn -> Reward.fetch_emission_rewards_tuples(address_hash, paging_options, blocks_range, options) end)
|
|
|
|
[rewards_task | address_to_transactions_tasks(address_hash, options, true)]
|
|
|> wait_for_address_transactions()
|
|
|> Enum.sort_by(fn item ->
|
|
case item do
|
|
{%Reward{} = emission_reward, _} ->
|
|
{-emission_reward.block.number, 1}
|
|
|
|
item ->
|
|
process_item(item)
|
|
end
|
|
end)
|
|
|> Enum.dedup_by(fn item ->
|
|
case item do
|
|
{%Reward{} = emission_reward, _} ->
|
|
{emission_reward.block_hash, emission_reward.address_hash, emission_reward.address_type}
|
|
|
|
transaction ->
|
|
transaction.hash
|
|
end
|
|
end)
|
|
|> Enum.take(paging_options.page_size)
|
|
end
|
|
|
|
defp process_item(item) do
|
|
block_number = if item.block_number, do: -item.block_number, else: 0
|
|
index = if item.index, do: -item.index, else: 0
|
|
{block_number, index}
|
|
end
|
|
|
|
def address_to_transactions_without_rewards(address_hash, options, old_ui? \\ true) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
address_hash
|
|
|> address_to_transactions_tasks(options, old_ui?)
|
|
|> wait_for_address_transactions()
|
|
|> Enum.sort_by(&{&1.block_number, &1.index}, &>=/2)
|
|
|> Enum.dedup_by(& &1.hash)
|
|
|> Enum.take(paging_options.page_size)
|
|
end
|
|
|
|
def address_hashes_to_mined_transactions_without_rewards(address_hashes, options) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
address_hashes
|
|
|> address_hashes_to_mined_transactions_tasks(options)
|
|
|> wait_for_address_transactions()
|
|
|> Enum.sort_by(&{&1.block_number, &1.index}, &>=/2)
|
|
|> Enum.dedup_by(& &1.hash)
|
|
|> Enum.take(paging_options.page_size)
|
|
end
|
|
|
|
defp address_to_transactions_tasks_query(options, only_mined? \\ false) do
|
|
from_block = from_block(options)
|
|
to_block = to_block(options)
|
|
|
|
options
|
|
|> Keyword.get(:paging_options, @default_paging_options)
|
|
|> fetch_transactions(from_block, to_block, !only_mined?)
|
|
end
|
|
|
|
defp transactions_block_numbers_at_address(address_hash, options) do
|
|
direction = Keyword.get(options, :direction)
|
|
|
|
options
|
|
|> address_to_transactions_tasks_query()
|
|
|> Transaction.not_pending_transactions()
|
|
|> select([t], t.block_number)
|
|
|> Transaction.matching_address_queries_list(direction, address_hash)
|
|
end
|
|
|
|
defp address_to_transactions_tasks(address_hash, options, old_ui?) do
|
|
direction = Keyword.get(options, :direction)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
options
|
|
|> address_to_transactions_tasks_query()
|
|
|> Transaction.not_dropped_or_replaced_transactions()
|
|
|> join_associations(necessity_by_association)
|
|
|> put_has_token_transfers_to_tx(old_ui?)
|
|
|> Transaction.matching_address_queries_list(direction, address_hash)
|
|
|> Enum.map(fn query -> Task.async(fn -> select_repo(options).all(query) end) end)
|
|
end
|
|
|
|
defp address_hashes_to_mined_transactions_tasks(address_hashes, options) do
|
|
direction = Keyword.get(options, :direction)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
options
|
|
|> address_to_transactions_tasks_query(true)
|
|
|> Transaction.not_pending_transactions()
|
|
|> join_associations(necessity_by_association)
|
|
|> put_has_token_transfers_to_tx(false)
|
|
|> Transaction.matching_address_queries_list(direction, address_hashes)
|
|
|> Enum.map(fn query -> Task.async(fn -> select_repo(options).all(query) end) end)
|
|
end
|
|
|
|
def address_to_transactions_tasks_range_of_blocks(address_hash, options) do
|
|
extremums_list =
|
|
address_hash
|
|
|> transactions_block_numbers_at_address(options)
|
|
|> Enum.map(fn query ->
|
|
extremum_query =
|
|
from(
|
|
q in subquery(query),
|
|
select: %{min_block_number: min(q.block_number), max_block_number: max(q.block_number)}
|
|
)
|
|
|
|
extremum_query
|
|
|> Repo.one!()
|
|
end)
|
|
|
|
extremums_list
|
|
|> Enum.reduce(%{min_block_number: nil, max_block_number: 0}, fn %{
|
|
min_block_number: min_number,
|
|
max_block_number: max_number
|
|
},
|
|
extremums_result ->
|
|
current_min_number = Map.get(extremums_result, :min_block_number)
|
|
current_max_number = Map.get(extremums_result, :max_block_number)
|
|
|
|
extremums_result
|
|
|> process_extremums_result_against_min_number(current_min_number, min_number)
|
|
|> process_extremums_result_against_max_number(current_max_number, max_number)
|
|
end)
|
|
end
|
|
|
|
defp process_extremums_result_against_min_number(extremums_result, current_min_number, min_number)
|
|
when is_number(current_min_number) and
|
|
not (is_number(min_number) and min_number > 0 and min_number < current_min_number) do
|
|
extremums_result
|
|
end
|
|
|
|
defp process_extremums_result_against_min_number(extremums_result, _current_min_number, min_number) do
|
|
extremums_result
|
|
|> Map.put(:min_block_number, min_number)
|
|
end
|
|
|
|
defp process_extremums_result_against_max_number(extremums_result, current_max_number, max_number)
|
|
when is_number(max_number) and max_number > 0 and max_number > current_max_number do
|
|
extremums_result
|
|
|> Map.put(:max_block_number, max_number)
|
|
end
|
|
|
|
defp process_extremums_result_against_max_number(extremums_result, _current_max_number, _max_number) do
|
|
extremums_result
|
|
end
|
|
|
|
defp wait_for_address_transactions(tasks) do
|
|
tasks
|
|
|> Task.yield_many(:timer.seconds(20))
|
|
|> Enum.flat_map(fn {_task, res} ->
|
|
case res do
|
|
{:ok, result} ->
|
|
result
|
|
|
|
{:exit, reason} ->
|
|
raise "Query fetching address transactions terminated: #{inspect(reason)}"
|
|
|
|
nil ->
|
|
raise "Query fetching address transactions timed out."
|
|
end
|
|
end)
|
|
end
|
|
|
|
@spec address_hash_to_token_transfers(Hash.Address.t(), Keyword.t()) :: [Transaction.t()]
|
|
def address_hash_to_token_transfers(address_hash, options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
direction = Keyword.get(options, :direction)
|
|
|
|
direction
|
|
|> Transaction.transactions_with_token_transfers_direction(address_hash)
|
|
|> Transaction.preload_token_transfers(address_hash)
|
|
|> handle_paging_options(paging_options)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec address_hash_to_token_transfers_new(Hash.Address.t() | String.t(), Keyword.t()) :: [TokenTransfer.t()]
|
|
def address_hash_to_token_transfers_new(address_hash, options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
direction = Keyword.get(options, :direction)
|
|
filters = Keyword.get(options, :token_type)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association)
|
|
|
|
direction
|
|
|> TokenTransfer.token_transfers_by_address_hash(address_hash, filters)
|
|
|> join_associations(necessity_by_association)
|
|
|> TokenTransfer.handle_paging_options(paging_options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec address_hash_to_token_transfers_by_token_address_hash(
|
|
Hash.Address.t() | String.t(),
|
|
Hash.Address.t() | String.t(),
|
|
Keyword.t()
|
|
) :: [TokenTransfer.t()]
|
|
def address_hash_to_token_transfers_by_token_address_hash(address_hash, token_address_hash, options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association)
|
|
|
|
address_hash
|
|
|> TokenTransfer.token_transfers_by_address_hash_and_token_address_hash(token_address_hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> TokenTransfer.handle_paging_options(paging_options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec address_hash_to_withdrawals(
|
|
Hash.Address.t(),
|
|
[paging_options | necessity_by_association_option]
|
|
) :: [Withdrawal.t()]
|
|
def address_hash_to_withdrawals(address_hash, options \\ []) when is_list(options) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
address_hash
|
|
|> Withdrawal.address_hash_to_withdrawals_query()
|
|
|> join_associations(necessity_by_association)
|
|
|> handle_withdrawals_paging_options(paging_options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec address_to_logs(Hash.Address.t(), Keyword.t()) :: [Log.t()]
|
|
def address_to_logs(address_hash, csv_export?, options \\ []) when is_list(options) do
|
|
paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50}
|
|
|
|
from_block = from_block(options)
|
|
to_block = to_block(options)
|
|
|
|
base =
|
|
from(log in Log,
|
|
order_by: [desc: log.block_number, desc: log.index],
|
|
where: log.address_hash == ^address_hash,
|
|
limit: ^paging_options.page_size,
|
|
select: log,
|
|
inner_join: block in Block,
|
|
on: block.hash == log.block_hash,
|
|
where: block.consensus
|
|
)
|
|
|
|
preloaded_query =
|
|
if csv_export? do
|
|
base
|
|
else
|
|
base
|
|
|> preload(transaction: [:to_address, :from_address])
|
|
end
|
|
|
|
preloaded_query
|
|
|> page_logs(paging_options)
|
|
|> filter_topic(Keyword.get(options, :topic))
|
|
|> where_block_number_in_period(from_block, to_block)
|
|
|> select_repo(options).all()
|
|
|> Enum.take(paging_options.page_size)
|
|
end
|
|
|
|
defp filter_topic(base_query, nil), do: base_query
|
|
|
|
defp filter_topic(base_query, ""), do: base_query
|
|
|
|
defp filter_topic(base_query, topic) do
|
|
from(log in base_query,
|
|
where:
|
|
log.first_topic == ^topic or log.second_topic == ^topic or log.third_topic == ^topic or
|
|
log.fourth_topic == ^topic
|
|
)
|
|
end
|
|
|
|
def where_block_number_in_period(base_query, from_block, to_block) when is_nil(from_block) and not is_nil(to_block) do
|
|
from(q in base_query,
|
|
where: q.block_number <= ^to_block
|
|
)
|
|
end
|
|
|
|
def where_block_number_in_period(base_query, from_block, to_block) when not is_nil(from_block) and is_nil(to_block) do
|
|
from(q in base_query,
|
|
where: q.block_number > ^from_block
|
|
)
|
|
end
|
|
|
|
def where_block_number_in_period(base_query, from_block, to_block) when is_nil(from_block) and is_nil(to_block) do
|
|
base_query
|
|
end
|
|
|
|
def where_block_number_in_period(base_query, from_block, to_block) do
|
|
from(q in base_query,
|
|
where: q.block_number > ^from_block and q.block_number <= ^to_block
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Finds all `t:Explorer.Chain.Transaction.t/0`s given the address_hash and the token contract
|
|
address hash.
|
|
|
|
## Options
|
|
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (in the form of `%{"inserted_at" => inserted_at}`). Results will be the transactions
|
|
older than the `index` that are passed.
|
|
"""
|
|
@spec address_to_transactions_with_token_transfers(Hash.t(), Hash.t(), [paging_options]) :: [Transaction.t()]
|
|
def address_to_transactions_with_token_transfers(address_hash, token_hash, options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
address_hash
|
|
|> Transaction.transactions_with_token_transfers(token_hash)
|
|
|> Transaction.preload_token_transfers(address_hash)
|
|
|> handle_paging_options(paging_options)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
The `t:Explorer.Chain.Address.t/0` `balance` in `unit`.
|
|
"""
|
|
@spec balance(Address.t(), :wei) :: Wei.wei() | nil
|
|
@spec balance(Address.t(), :gwei) :: Wei.gwei() | nil
|
|
@spec balance(Address.t(), :ether) :: Wei.ether() | nil
|
|
def balance(%Address{fetched_coin_balance: balance}, unit) do
|
|
case balance do
|
|
nil -> nil
|
|
_ -> Wei.to(balance, unit)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
The number of `t:Explorer.Chain.Block.t/0`.
|
|
|
|
iex> insert_list(2, :block)
|
|
iex> Explorer.Chain.block_count()
|
|
2
|
|
|
|
When there are no `t:Explorer.Chain.Block.t/0`.
|
|
|
|
iex> Explorer.Chain.block_count()
|
|
0
|
|
|
|
"""
|
|
def block_count do
|
|
Repo.aggregate(Block, :count, :hash)
|
|
end
|
|
|
|
@doc """
|
|
Reward for mining a block.
|
|
|
|
The block reward is the sum of the following:
|
|
|
|
* Sum of the transaction fees (gas_used * gas_price) for the block
|
|
* A static reward for miner (this value may change during the life of the chain)
|
|
* The reward for uncle blocks (1/32 * static_reward * number_of_uncles)
|
|
|
|
*NOTE*
|
|
|
|
Uncles are not currently accounted for.
|
|
"""
|
|
@spec block_reward(Block.block_number()) :: Wei.t()
|
|
def block_reward(block_number) do
|
|
block_hash =
|
|
Block
|
|
|> where([block], block.number == ^block_number and block.consensus == true)
|
|
|> select([block], block.hash)
|
|
|> Repo.one!()
|
|
|
|
case Repo.one!(
|
|
from(reward in Reward,
|
|
where: reward.block_hash == ^block_hash,
|
|
select: %Wei{
|
|
value: coalesce(sum(reward.reward), 0)
|
|
}
|
|
)
|
|
) do
|
|
%Wei{
|
|
value: %Decimal{coef: 0}
|
|
} ->
|
|
Repo.one!(
|
|
from(block in Block,
|
|
left_join: transaction in assoc(block, :transactions),
|
|
inner_join: emission_reward in EmissionReward,
|
|
on: fragment("? <@ ?", block.number, emission_reward.block_range),
|
|
where: block.number == ^block_number and block.consensus == true,
|
|
group_by: [emission_reward.reward, block.hash],
|
|
select: %Wei{
|
|
value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0) + emission_reward.reward
|
|
}
|
|
)
|
|
)
|
|
|
|
other_value ->
|
|
other_value
|
|
end
|
|
end
|
|
|
|
def txn_fees(transactions) do
|
|
Enum.reduce(transactions, Decimal.new(0), fn %{gas_used: gas_used, gas_price: gas_price}, acc ->
|
|
gas_used
|
|
|> Decimal.new()
|
|
|> Decimal.mult(gas_price_to_decimal(gas_price))
|
|
|> Decimal.add(acc)
|
|
end)
|
|
end
|
|
|
|
defp gas_price_to_decimal(%Wei{} = wei), do: wei.value
|
|
defp gas_price_to_decimal(gas_price), do: Decimal.new(gas_price)
|
|
|
|
def burned_fees(transactions, base_fee_per_gas) do
|
|
burned_fee_counter =
|
|
transactions
|
|
|> Enum.reduce(Decimal.new(0), fn %{gas_used: gas_used}, acc ->
|
|
gas_used
|
|
|> Decimal.new()
|
|
|> Decimal.add(acc)
|
|
end)
|
|
|
|
base_fee_per_gas && Wei.mult(base_fee_per_gas_to_wei(base_fee_per_gas), burned_fee_counter)
|
|
end
|
|
|
|
defp base_fee_per_gas_to_wei(%Wei{} = wei), do: wei
|
|
defp base_fee_per_gas_to_wei(base_fee_per_gas), do: %Wei{value: Decimal.new(base_fee_per_gas)}
|
|
|
|
@uncle_reward_coef 1 / 32
|
|
def block_reward_by_parts(block, transactions) do
|
|
%{hash: block_hash, number: block_number} = block
|
|
base_fee_per_gas = Map.get(block, :base_fee_per_gas)
|
|
|
|
txn_fees = txn_fees(transactions)
|
|
|
|
static_reward =
|
|
Repo.one(
|
|
from(
|
|
er in EmissionReward,
|
|
where: fragment("int8range(?, ?) <@ ?", ^block_number, ^(block_number + 1), er.block_range),
|
|
select: er.reward
|
|
)
|
|
) || %Wei{value: Decimal.new(0)}
|
|
|
|
has_uncles? = is_list(block.uncles) and not Enum.empty?(block.uncles)
|
|
|
|
burned_fees = burned_fees(transactions, base_fee_per_gas)
|
|
uncle_reward = (has_uncles? && Wei.mult(static_reward, Decimal.from_float(@uncle_reward_coef))) || nil
|
|
|
|
%{
|
|
block_number: block_number,
|
|
block_hash: block_hash,
|
|
miner_hash: block.miner_hash,
|
|
static_reward: static_reward,
|
|
txn_fees: %Wei{value: txn_fees},
|
|
burned_fees: burned_fees || %Wei{value: Decimal.new(0)},
|
|
uncle_reward: uncle_reward || %Wei{value: Decimal.new(0)}
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
The `t:Explorer.Chain.Wei.t/0` paid to the miners of the `t:Explorer.Chain.Block.t/0`s with `hash`
|
|
`Explorer.Chain.Hash.Full.t/0` by the signers of the transactions in those blocks to cover the gas fee
|
|
(`gas_used * gas_price`).
|
|
"""
|
|
@spec gas_payment_by_block_hash([Hash.Full.t()]) :: %{Hash.Full.t() => Wei.t()}
|
|
def gas_payment_by_block_hash(block_hashes) when is_list(block_hashes) do
|
|
query =
|
|
from(
|
|
block in Block,
|
|
left_join: transaction in assoc(block, :transactions),
|
|
where: block.hash in ^block_hashes and block.consensus == true,
|
|
group_by: block.hash,
|
|
select: {block.hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}}
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> Enum.into(%{})
|
|
end
|
|
|
|
def timestamp_by_block_hash(block_hashes) when is_list(block_hashes) do
|
|
query =
|
|
from(
|
|
block in Block,
|
|
where: block.hash in ^block_hashes and block.consensus == true,
|
|
group_by: block.hash,
|
|
select: {block.hash, block.timestamp}
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> Enum.into(%{})
|
|
end
|
|
|
|
@doc """
|
|
Finds all `t:Explorer.Chain.Transaction.t/0`s 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`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{index}`) and. Results will be the transactions older than
|
|
the `index` that are passed.
|
|
"""
|
|
@spec block_to_transactions(Hash.Full.t(), [paging_options | necessity_by_association_option | api?()], true | false) ::
|
|
[
|
|
Transaction.t()
|
|
]
|
|
def block_to_transactions(block_hash, options \\ [], old_ui? \\ true) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
options
|
|
|> Keyword.get(:paging_options, @default_paging_options)
|
|
|> fetch_transactions_in_ascending_order_by_index()
|
|
|> join(:inner, [transaction], block in assoc(transaction, :block))
|
|
|> where([_, block], block.hash == ^block_hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> put_has_token_transfers_to_tx(old_ui?)
|
|
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|
|
|> select_repo(options).all()
|
|
|> (&if(old_ui?,
|
|
do: &1,
|
|
else:
|
|
Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_necessity_by_association, options) end)
|
|
)).()
|
|
end
|
|
|
|
@spec block_to_withdrawals(
|
|
Hash.Full.t(),
|
|
[paging_options | necessity_by_association_option]
|
|
) :: [Withdrawal.t()]
|
|
def block_to_withdrawals(block_hash, options \\ []) when is_list(options) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
block_hash
|
|
|> Withdrawal.block_hash_to_withdrawals_query()
|
|
|> join_associations(necessity_by_association)
|
|
|> handle_withdrawals_paging_options(paging_options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Finds sum of gas_used for new (EIP-1559) txs belongs to block
|
|
"""
|
|
@spec block_to_gas_used_by_1559_txs(Hash.Full.t()) :: non_neg_integer()
|
|
def block_to_gas_used_by_1559_txs(block_hash) do
|
|
query =
|
|
from(
|
|
tx in Transaction,
|
|
where: tx.block_hash == ^block_hash,
|
|
select: sum(tx.gas_used)
|
|
)
|
|
|
|
result = Repo.one(query)
|
|
if result, do: result, else: 0
|
|
end
|
|
|
|
@doc """
|
|
Finds sum of priority fee for new (EIP-1559) txs belongs to block
|
|
"""
|
|
@spec block_to_priority_fee_of_1559_txs(Hash.Full.t()) :: Decimal.t()
|
|
def block_to_priority_fee_of_1559_txs(block_hash) do
|
|
block = Repo.get_by(Block, hash: block_hash)
|
|
|
|
case block.base_fee_per_gas do
|
|
%Wei{value: base_fee_per_gas} ->
|
|
query =
|
|
from(
|
|
tx in Transaction,
|
|
where: tx.block_hash == ^block_hash,
|
|
select:
|
|
sum(
|
|
fragment(
|
|
"CASE
|
|
WHEN COALESCE(?,?) = 0 THEN 0
|
|
WHEN COALESCE(?,?) - ? < COALESCE(?,?) THEN (COALESCE(?,?) - ?) * ?
|
|
ELSE COALESCE(?,?) * ? END",
|
|
tx.max_fee_per_gas,
|
|
tx.gas_price,
|
|
tx.max_fee_per_gas,
|
|
tx.gas_price,
|
|
^base_fee_per_gas,
|
|
tx.max_priority_fee_per_gas,
|
|
tx.gas_price,
|
|
tx.max_fee_per_gas,
|
|
tx.gas_price,
|
|
^base_fee_per_gas,
|
|
tx.gas_used,
|
|
tx.max_priority_fee_per_gas,
|
|
tx.gas_price,
|
|
tx.gas_used
|
|
)
|
|
)
|
|
)
|
|
|
|
result = Repo.one(query)
|
|
if result, do: result, else: 0
|
|
|
|
_ ->
|
|
0
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Counts the number of `t:Explorer.Chain.Transaction.t/0` in the `block`.
|
|
"""
|
|
@spec block_to_transaction_count(Hash.Full.t()) :: non_neg_integer()
|
|
def block_to_transaction_count(block_hash) do
|
|
query =
|
|
from(
|
|
transaction in Transaction,
|
|
where: transaction.block_hash == ^block_hash
|
|
)
|
|
|
|
Repo.aggregate(query, :count, :hash)
|
|
end
|
|
|
|
@spec check_if_withdrawals_in_block(Hash.Full.t()) :: boolean()
|
|
def check_if_withdrawals_in_block(block_hash, options \\ []) do
|
|
block_hash
|
|
|> Withdrawal.block_hash_to_withdrawals_unordered_query()
|
|
|> select_repo(options).exists?()
|
|
end
|
|
|
|
@doc """
|
|
How many blocks have confirmed `block` based on the current `max_block_number`
|
|
|
|
A consensus block's number of confirmations is the difference between its number and the current block height + 1.
|
|
|
|
iex> block = insert(:block, number: 1)
|
|
iex> Explorer.Chain.confirmations(block, block_height: 2)
|
|
{:ok, 2}
|
|
|
|
The newest block at the block height has 1 confirmation.
|
|
|
|
iex> block = insert(:block, number: 1)
|
|
iex> Explorer.Chain.confirmations(block, block_height: 1)
|
|
{:ok, 1}
|
|
|
|
A non-consensus block has no confirmations and is orphaned even if there are child blocks of it on an orphaned chain.
|
|
|
|
iex> parent_block = insert(:block, consensus: false, number: 1)
|
|
iex> insert(
|
|
...> :block,
|
|
...> parent_hash: parent_block.hash,
|
|
...> consensus: false,
|
|
...> number: parent_block.number + 1
|
|
...> )
|
|
iex> Explorer.Chain.confirmations(parent_block, block_height: 3)
|
|
{:error, :non_consensus}
|
|
|
|
If you calculate the block height and then get a newer block, the confirmations will be `0` instead of negative.
|
|
|
|
iex> block = insert(:block, number: 1)
|
|
iex> Explorer.Chain.confirmations(block, block_height: 0)
|
|
{:ok, 1}
|
|
"""
|
|
@spec confirmations(Block.t() | nil, [{:block_height, block_height()}]) ::
|
|
{:ok, non_neg_integer()} | {:error, :non_consensus | :pending}
|
|
|
|
def confirmations(%Block{consensus: true, number: number}, named_arguments) when is_list(named_arguments) do
|
|
max_consensus_block_number = Keyword.fetch!(named_arguments, :block_height)
|
|
|
|
{:ok, max(1 + max_consensus_block_number - number, 1)}
|
|
end
|
|
|
|
def confirmations(%Block{consensus: false}, _), do: {:error, :non_consensus}
|
|
|
|
def confirmations(nil, _), do: {:error, :pending}
|
|
|
|
@doc """
|
|
Creates an address.
|
|
|
|
iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"}
|
|
...> )
|
|
...> to_string(hash)
|
|
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"
|
|
|
|
A `String.t/0` value for `Explorer.Chain.Address.t/0` `hash` must have 40 hexadecimal characters after the `0x` prefix
|
|
to prevent short- and long-hash transcription errors.
|
|
|
|
iex> {:error, %Ecto.Changeset{errors: errors}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0"}
|
|
...> )
|
|
...> errors
|
|
[hash: {"is invalid", [type: Explorer.Chain.Hash.Address, validation: :cast]}]
|
|
iex> {:error, %Ecto.Changeset{errors: errors}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0ba"}
|
|
...> )
|
|
...> errors
|
|
[hash: {"is invalid", [type: Explorer.Chain.Hash.Address, validation: :cast]}]
|
|
|
|
"""
|
|
@spec create_address(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
|
|
def create_address(attrs \\ %{}) do
|
|
%Address{}
|
|
|> Address.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Creates a decompiled smart contract.
|
|
"""
|
|
|
|
@spec create_decompiled_smart_contract(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
|
|
def create_decompiled_smart_contract(attrs) do
|
|
changeset = DecompiledSmartContract.changeset(%DecompiledSmartContract{}, attrs)
|
|
|
|
# Enforce ShareLocks tables order (see docs: sharelocks.md)
|
|
Multi.new()
|
|
|> Multi.run(:set_address_decompiled, fn repo, _ ->
|
|
set_address_decompiled(repo, Changeset.get_field(changeset, :address_hash))
|
|
end)
|
|
|> Multi.insert(:decompiled_smart_contract, changeset,
|
|
on_conflict: :replace_all,
|
|
conflict_target: [:decompiler_version, :address_hash]
|
|
)
|
|
|> Repo.transaction()
|
|
|> case do
|
|
{:ok, %{decompiled_smart_contract: decompiled_smart_contract}} -> {:ok, decompiled_smart_contract}
|
|
{:error, _, error_value, _} -> {:error, error_value}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users efficiently.
|
|
|
|
iex> %Explorer.Chain.Data{
|
|
...> bytes: <<>>
|
|
...> } |>
|
|
...> Explorer.Chain.data_to_iodata() |>
|
|
...> IO.iodata_to_binary()
|
|
"0x"
|
|
iex> %Explorer.Chain.Data{
|
|
...> bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7,
|
|
...> 115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26,
|
|
...> 179, 239>>
|
|
...> } |>
|
|
...> Explorer.Chain.data_to_iodata() |>
|
|
...> IO.iodata_to_binary()
|
|
"0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef"
|
|
|
|
"""
|
|
@spec data_to_iodata(Data.t()) :: iodata()
|
|
def data_to_iodata(data) do
|
|
Data.to_iodata(data)
|
|
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: %Explorer.Chain.Wei{value: Decimal.new(2)},
|
|
...> gas_used: 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 `transaction`.
|
|
|
|
iex> Explorer.Chain.fee(
|
|
...> %Explorer.Chain.Transaction{
|
|
...> gas: Decimal.new(3),
|
|
...> gas_price: %Explorer.Chain.Wei{value: Decimal.new(2)},
|
|
...> gas_used: Decimal.new(2)
|
|
...> },
|
|
...> :wei
|
|
...> )
|
|
{:actual, Decimal.new(4)}
|
|
|
|
"""
|
|
@spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t()} | {:actual, Decimal.t()}
|
|
def fee(%Transaction{gas: gas, gas_price: gas_price, gas_used: nil}, unit) do
|
|
fee =
|
|
gas_price
|
|
|> Wei.to(unit)
|
|
|> Decimal.mult(gas)
|
|
|
|
{:maximum, fee}
|
|
end
|
|
|
|
def fee(%Transaction{gas_price: gas_price, gas_used: gas_used}, unit) do
|
|
fee =
|
|
gas_price
|
|
|> Wei.to(unit)
|
|
|> Decimal.mult(gas_used)
|
|
|
|
{:actual, fee}
|
|
end
|
|
|
|
@doc """
|
|
Checks to see if the chain is down indexing based on the transaction from the
|
|
oldest block and the pending operation
|
|
"""
|
|
@spec finished_indexing_internal_transactions?([api?]) :: boolean()
|
|
def finished_indexing_internal_transactions?(options \\ []) do
|
|
internal_transactions_disabled? =
|
|
Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor)[:disabled?] or
|
|
not Application.get_env(:indexer, Indexer.Supervisor)[:enabled]
|
|
|
|
if internal_transactions_disabled? do
|
|
true
|
|
else
|
|
json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments)
|
|
variant = Keyword.fetch!(json_rpc_named_arguments, :variant)
|
|
|
|
if variant == EthereumJSONRPC.Ganache || variant == EthereumJSONRPC.Arbitrum do
|
|
true
|
|
else
|
|
check_left_blocks_to_index_internal_transactions(options)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp check_left_blocks_to_index_internal_transactions(options) do
|
|
with {:transactions_exist, true} <- {:transactions_exist, select_repo(options).exists?(Transaction)},
|
|
min_block_number when not is_nil(min_block_number) <-
|
|
select_repo(options).aggregate(Transaction, :min, :block_number) do
|
|
min_block_number =
|
|
min_block_number
|
|
|> Decimal.max(EthereumJSONRPC.first_block_to_fetch(:trace_first_block))
|
|
|> Decimal.to_integer()
|
|
|
|
query =
|
|
from(
|
|
block in Block,
|
|
join: pending_ops in assoc(block, :pending_operations),
|
|
where: block.consensus and block.number == ^min_block_number
|
|
)
|
|
|
|
if select_repo(options).exists?(query) do
|
|
false
|
|
else
|
|
check_indexing_internal_transactions_threshold()
|
|
end
|
|
else
|
|
{:transactions_exist, false} -> true
|
|
nil -> false
|
|
end
|
|
end
|
|
|
|
defp check_indexing_internal_transactions_threshold do
|
|
pbo_count = PendingBlockOperationCache.estimated_count()
|
|
|
|
if pbo_count <
|
|
Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction)[:indexing_finished_threshold] do
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def finished_indexing_from_ratio?(ratio) do
|
|
Decimal.compare(ratio, 1) !== :lt
|
|
end
|
|
|
|
@doc """
|
|
Checks if indexing of blocks and internal transactions finished aka full indexing
|
|
"""
|
|
@spec finished_indexing?([api?]) :: boolean()
|
|
def finished_indexing?(options \\ []) do
|
|
if Application.get_env(:indexer, Indexer.Supervisor)[:enabled] do
|
|
indexed_ratio = indexed_ratio_blocks()
|
|
|
|
case finished_indexing_from_ratio?(indexed_ratio) do
|
|
false -> false
|
|
_ -> finished_indexing_internal_transactions?(options)
|
|
end
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
The `t:Explorer.Chain.Transaction.t/0` `gas_price` of the `transaction` in `unit`.
|
|
"""
|
|
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> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}
|
|
...> )
|
|
iex> {:ok, %Explorer.Chain.Address{hash: found_hash}} = Explorer.Chain.hash_to_address(hash)
|
|
iex> found_hash == hash
|
|
true
|
|
|
|
Returns `{:error, :not_found}` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.hash_to_address(hash)
|
|
{: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.Address.t/0` has no associated record for that association,
|
|
then the `t:Explorer.Chain.Address.t/0` will not be included in the list.
|
|
|
|
Optionally it also accepts a boolean to fetch the `has_decompiled_code?` virtual field or not
|
|
|
|
"""
|
|
@spec hash_to_address(Hash.Address.t(), [necessity_by_association_option | api?], boolean()) ::
|
|
{:ok, Address.t()} | {:error, :not_found}
|
|
def hash_to_address(
|
|
%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash,
|
|
options \\ [
|
|
necessity_by_association: %{
|
|
:contracts_creation_internal_transaction => :optional,
|
|
:names => :optional,
|
|
:smart_contract => :optional,
|
|
:token => :optional,
|
|
:contracts_creation_transaction => :optional
|
|
}
|
|
],
|
|
query_decompiled_code_flag \\ true
|
|
) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^hash
|
|
)
|
|
|
|
address_result =
|
|
query
|
|
|> join_associations(necessity_by_association)
|
|
|> with_decompiled_code_flag(hash, query_decompiled_code_flag)
|
|
|> select_repo(options).one()
|
|
|
|
address_updated_result =
|
|
case address_result do
|
|
%{smart_contract: smart_contract} ->
|
|
if smart_contract do
|
|
address_result
|
|
else
|
|
compose_smart_contract(address_result, hash, options)
|
|
end
|
|
|
|
_ ->
|
|
address_result
|
|
end
|
|
|
|
address_updated_result
|
|
|> case do
|
|
nil -> {:error, :not_found}
|
|
address -> {:ok, address}
|
|
end
|
|
end
|
|
|
|
defp compose_smart_contract(address_result, hash, options) do
|
|
address_verified_twin_contract =
|
|
get_minimal_proxy_template(hash, options) ||
|
|
get_address_verified_twin_contract(hash, options).verified_contract
|
|
|
|
if address_verified_twin_contract do
|
|
address_verified_twin_contract_updated =
|
|
address_verified_twin_contract
|
|
|> Map.put(:address_hash, hash)
|
|
|> Map.put(:metadata_from_verified_twin, true)
|
|
|> Map.put(:implementation_address_hash, nil)
|
|
|> Map.put(:implementation_name, nil)
|
|
|> Map.put(:implementation_fetched_at, nil)
|
|
|
|
address_result
|
|
|> Map.put(:smart_contract, address_verified_twin_contract_updated)
|
|
else
|
|
address_result
|
|
end
|
|
end
|
|
|
|
def decompiled_code(address_hash, version) do
|
|
query =
|
|
from(contract in DecompiledSmartContract,
|
|
where: contract.address_hash == ^address_hash and contract.decompiler_version == ^version
|
|
)
|
|
|
|
query
|
|
|> Repo.one()
|
|
|> case do
|
|
nil -> {:error, :not_found}
|
|
contract -> {:ok, contract.decompiled_source_code}
|
|
end
|
|
end
|
|
|
|
@spec token_contract_address_from_token_name(String.t()) :: {:ok, Hash.Address.t()} | {:error, :not_found}
|
|
def token_contract_address_from_token_name(name) when is_binary(name) do
|
|
query =
|
|
from(token in Token,
|
|
where: ilike(token.symbol, ^name),
|
|
or_where: ilike(token.name, ^name),
|
|
select: token.contract_address_hash
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> case do
|
|
[] ->
|
|
{:error, :not_found}
|
|
|
|
hashes ->
|
|
if Enum.count(hashes) == 1 do
|
|
{:ok, List.first(hashes)}
|
|
else
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
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> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}
|
|
...> )
|
|
iex> {:ok, %Explorer.Chain.Address{hash: found_hash}} = Explorer.Chain.hash_to_address(hash)
|
|
iex> found_hash == hash
|
|
true
|
|
|
|
Returns `{:error, address}` if not found but created an address
|
|
|
|
iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}
|
|
...> )
|
|
iex> {:ok, %Explorer.Chain.Address{hash: found_hash}} = Explorer.Chain.hash_to_address(hash)
|
|
iex> found_hash == hash
|
|
true
|
|
|
|
|
|
## Options
|
|
|
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
|
|
`:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association,
|
|
then the `t:Explorer.Chain.Address.t/0` will not be included in the list.
|
|
|
|
Optionally it also accepts a boolean to fetch the `has_decompiled_code?` virtual field or not
|
|
|
|
"""
|
|
@spec find_or_insert_address_from_hash(Hash.Address.t(), [necessity_by_association_option], boolean()) ::
|
|
{:ok, Address.t()}
|
|
def find_or_insert_address_from_hash(
|
|
%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash,
|
|
options \\ [
|
|
necessity_by_association: %{
|
|
:contracts_creation_internal_transaction => :optional,
|
|
:names => :optional,
|
|
:smart_contract => :optional,
|
|
:token => :optional,
|
|
:contracts_creation_transaction => :optional
|
|
}
|
|
],
|
|
query_decompiled_code_flag \\ true
|
|
) do
|
|
case hash_to_address(hash, options, query_decompiled_code_flag) do
|
|
{:ok, address} ->
|
|
{:ok, address}
|
|
|
|
{:error, :not_found} ->
|
|
create_address(%{hash: to_string(hash)})
|
|
hash_to_address(hash, options, query_decompiled_code_flag)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts list of `t:Explorer.Chain.Address.t/0` `hash` to the `t:Explorer.Chain.Address.t/0` with that `hash`.
|
|
|
|
Returns `[%Explorer.Chain.Address{}]}` if found
|
|
|
|
"""
|
|
@spec hashes_to_addresses([Hash.Address.t()]) :: [Address.t()]
|
|
def hashes_to_addresses(hashes) when is_list(hashes) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash in ^hashes,
|
|
# https://stackoverflow.com/a/29598910/470451
|
|
order_by: fragment("array_position(?, ?)", type(^hashes, {:array, Hash.Address}), address.hash)
|
|
)
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
Finds an `t:Explorer.Chain.Address.t/0` that has the provided `t:Explorer.Chain.Address.t/0` `hash` and a contract.
|
|
|
|
## Options
|
|
|
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
|
|
`:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association,
|
|
then the `t:Explorer.Chain.Address.t/0` will not be included in the list.
|
|
|
|
Optionally it also accepts a boolean to fetch the `has_decompiled_code?` virtual field or not
|
|
|
|
"""
|
|
@spec find_contract_address(Hash.Address.t(), [necessity_by_association_option], boolean()) ::
|
|
{:ok, Address.t()} | {:error, :not_found}
|
|
def find_contract_address(
|
|
%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash,
|
|
options \\ [],
|
|
query_decompiled_code_flag \\ false
|
|
) do
|
|
necessity_by_association =
|
|
options
|
|
|> Keyword.get(:necessity_by_association, %{})
|
|
|> Map.merge(%{
|
|
smart_contract_additional_sources: :optional
|
|
})
|
|
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^hash and not is_nil(address.contract_code)
|
|
)
|
|
|
|
address_result =
|
|
query
|
|
|> join_associations(necessity_by_association)
|
|
|> with_decompiled_code_flag(hash, query_decompiled_code_flag)
|
|
|> select_repo(options).one()
|
|
|
|
address_updated_result =
|
|
case address_result do
|
|
%{smart_contract: smart_contract} ->
|
|
if smart_contract do
|
|
CheckBytecodeMatchingOnDemand.trigger_check(address_result, smart_contract)
|
|
LookUpSmartContractSourcesOnDemand.trigger_fetch(address_result, smart_contract)
|
|
check_and_update_constructor_args(address_result)
|
|
else
|
|
LookUpSmartContractSourcesOnDemand.trigger_fetch(address_result, nil)
|
|
|
|
address_verified_twin_contract =
|
|
get_minimal_proxy_template(hash, options) ||
|
|
get_address_verified_twin_contract(hash, options).verified_contract
|
|
|
|
add_twin_info_to_contract(address_result, address_verified_twin_contract, hash)
|
|
end
|
|
|
|
_ ->
|
|
LookUpSmartContractSourcesOnDemand.trigger_fetch(address_result, nil)
|
|
address_result
|
|
end
|
|
|
|
address_updated_result
|
|
|> case do
|
|
nil -> {:error, :not_found}
|
|
address -> {:ok, address}
|
|
end
|
|
end
|
|
|
|
defp check_and_update_constructor_args(
|
|
%SmartContract{address_hash: address_hash, constructor_arguments: nil, verified_via_sourcify: true} =
|
|
smart_contract
|
|
) do
|
|
if args = Verifier.parse_constructor_arguments_for_sourcify_contract(address_hash, smart_contract.abi) do
|
|
smart_contract |> SmartContract.changeset(%{constructor_arguments: args}) |> Repo.update()
|
|
%SmartContract{smart_contract | constructor_arguments: args}
|
|
else
|
|
smart_contract
|
|
end
|
|
end
|
|
|
|
defp check_and_update_constructor_args(
|
|
%Address{
|
|
hash: address_hash,
|
|
contract_code: deployed_bytecode,
|
|
smart_contract: %SmartContract{constructor_arguments: nil, verified_via_sourcify: true} = smart_contract
|
|
} = address
|
|
) do
|
|
if args =
|
|
Verifier.parse_constructor_arguments_for_sourcify_contract(address_hash, smart_contract.abi, deployed_bytecode) do
|
|
smart_contract |> SmartContract.changeset(%{constructor_arguments: args}) |> Repo.update()
|
|
%Address{address | smart_contract: %SmartContract{smart_contract | constructor_arguments: args}}
|
|
else
|
|
address
|
|
end
|
|
end
|
|
|
|
defp check_and_update_constructor_args(other), do: other
|
|
|
|
defp add_twin_info_to_contract(address_result, address_verified_twin_contract, _hash)
|
|
when is_nil(address_verified_twin_contract),
|
|
do: address_result
|
|
|
|
defp add_twin_info_to_contract(address_result, address_verified_twin_contract, hash) do
|
|
address_verified_twin_contract_updated =
|
|
address_verified_twin_contract
|
|
|> Map.put(:address_hash, hash)
|
|
|> Map.put(:metadata_from_verified_twin, true)
|
|
|> Map.put(:implementation_address_hash, nil)
|
|
|> Map.put(:implementation_name, nil)
|
|
|> Map.put(:implementation_fetched_at, nil)
|
|
|
|
address_result
|
|
|> Map.put(:smart_contract, address_verified_twin_contract_updated)
|
|
end
|
|
|
|
@spec find_decompiled_contract_address(Hash.Address.t()) :: {:ok, Address.t()} | {:error, :not_found}
|
|
def find_decompiled_contract_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
preload: [
|
|
:contracts_creation_internal_transaction,
|
|
:names,
|
|
:smart_contract,
|
|
:token,
|
|
:contracts_creation_transaction,
|
|
:decompiled_smart_contracts
|
|
],
|
|
where: address.hash == ^hash
|
|
)
|
|
|
|
address = Repo.one(query)
|
|
|
|
if address do
|
|
{:ok, address}
|
|
else
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts `t:Explorer.Chain.Block.t/0` `hash` to the `t:Explorer.Chain.Block.t/0` with that `hash`.
|
|
|
|
Unlike `number_to_block/1`, both consensus and non-consensus blocks can be returned when looked up by `hash`.
|
|
|
|
Returns `{:ok, %Explorer.Chain.Block{}}` if found
|
|
|
|
iex> %Block{hash: hash} = insert(:block, consensus: false)
|
|
iex> {:ok, %Explorer.Chain.Block{hash: found_hash}} = Explorer.Chain.hash_to_block(hash)
|
|
iex> found_hash == hash
|
|
true
|
|
|
|
Returns `{:error, :not_found}` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_block_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
iex> Explorer.Chain.hash_to_block(hash)
|
|
{: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.Block.t/0` has no associated record for that association, then the
|
|
`t:Explorer.Chain.Block.t/0` will not be included in the page `entries`.
|
|
|
|
"""
|
|
@spec hash_to_block(Hash.Full.t(), [necessity_by_association_option | api?]) ::
|
|
{:ok, Block.t()} | {:error, :not_found}
|
|
def hash_to_block(%Hash{byte_count: unquote(Hash.Full.byte_count())} = hash, options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
Block
|
|
|> where(hash: ^hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).one()
|
|
|> case do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
block ->
|
|
{:ok, block}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the `Explorer.Chain.Hash.t:t/0` to `iodata` representation that can be written efficiently to users.
|
|
|
|
iex> %Explorer.Chain.Hash{
|
|
...> byte_count: 32,
|
|
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
|
|
...> big-integer-size(32)-unit(8)>>
|
|
...> } |>
|
|
...> Explorer.Chain.hash_to_iodata() |>
|
|
...> IO.iodata_to_binary()
|
|
"0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
|
|
Always pads number, so that it is a valid format for casting.
|
|
|
|
iex> %Explorer.Chain.Hash{
|
|
...> byte_count: 32,
|
|
...> bytes: <<0x1234567890abcdef :: big-integer-size(32)-unit(8)>>
|
|
...> } |>
|
|
...> Explorer.Chain.hash_to_iodata() |>
|
|
...> IO.iodata_to_binary()
|
|
"0x0000000000000000000000000000000000000000000000001234567890abcdef"
|
|
|
|
"""
|
|
@spec hash_to_iodata(Hash.t()) :: iodata()
|
|
def hash_to_iodata(hash) do
|
|
Hash.to_iodata(hash)
|
|
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> %Transaction{hash: hash} = insert(:transaction)
|
|
iex> {:ok, %Explorer.Chain.Transaction{hash: found_hash}} = Explorer.Chain.hash_to_transaction(hash)
|
|
iex> found_hash == hash
|
|
true
|
|
|
|
Returns `{:error, :not_found}` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
iex> Explorer.Chain.hash_to_transaction(hash)
|
|
{: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(Hash.Full.t(), [necessity_by_association_option | api?]) ::
|
|
{:ok, Transaction.t()} | {:error, :not_found}
|
|
def hash_to_transaction(
|
|
%Hash{byte_count: unquote(Hash.Full.byte_count())} = hash,
|
|
options \\ []
|
|
)
|
|
when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
Transaction
|
|
|> where(hash: ^hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).one()
|
|
|> case do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
transaction ->
|
|
{:ok, transaction}
|
|
end
|
|
end
|
|
|
|
# preload_to_detect_tt?: we don't need to preload more than one token transfer in case the tx inside the list (we don't show any token transfers on tx tile in new UI)
|
|
def preload_token_transfers(
|
|
%Transaction{hash: tx_hash, block_hash: block_hash} = transaction,
|
|
necessity_by_association,
|
|
options,
|
|
preload_to_detect_tt? \\ true
|
|
) do
|
|
if preload_to_detect_tt? do
|
|
transaction
|
|
else
|
|
limit = if(preload_to_detect_tt?, do: 1, else: @token_transfers_per_transaction_preview + 1)
|
|
|
|
token_transfers =
|
|
TokenTransfer
|
|
|> (&if(is_nil(block_hash),
|
|
do: where(&1, [token_transfer], token_transfer.transaction_hash == ^tx_hash),
|
|
else:
|
|
where(
|
|
&1,
|
|
[token_transfer],
|
|
token_transfer.transaction_hash == ^tx_hash and token_transfer.block_hash == ^block_hash
|
|
)
|
|
)).()
|
|
|> limit(^limit)
|
|
|> order_by([token_transfer], asc: token_transfer.log_index)
|
|
|> (&if(preload_to_detect_tt?, do: &1, else: join_associations(&1, necessity_by_association))).()
|
|
|> select_repo(options).all()
|
|
|> flat_1155_batch_token_transfers()
|
|
|> Enum.take(limit)
|
|
|
|
%Transaction{transaction | token_transfers: token_transfers}
|
|
end
|
|
end
|
|
|
|
def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
|
|
|
|
@doc """
|
|
Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for
|
|
those `hashes`.
|
|
|
|
Returns list of `%Explorer.Chain.Transaction{}`s if found
|
|
|
|
iex> [%Transaction{hash: hash1}, %Transaction{hash: hash2}] = insert_list(2, :transaction)
|
|
iex> [%Explorer.Chain.Transaction{hash: found_hash1}, %Explorer.Chain.Transaction{hash: found_hash2}] =
|
|
...> Explorer.Chain.hashes_to_transactions([hash1, hash2])
|
|
iex> found_hash1 in [hash1, hash2]
|
|
true
|
|
iex> found_hash2 in [hash1, hash2]
|
|
true
|
|
|
|
Returns `[]` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
iex> Explorer.Chain.hashes_to_transactions([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.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 hashes_to_transactions([Hash.Full.t()], [necessity_by_association_option]) :: [Transaction.t()] | []
|
|
def hashes_to_transactions(hashes, options \\ []) when is_list(hashes) and is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
fetch_transactions()
|
|
|> where([transaction], transaction.hash in ^hashes)
|
|
|> join_associations(necessity_by_association)
|
|
|> preload([{:token_transfers, [:token, :from_address, :to_address]}])
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Bulk insert all data stored in the `Explorer`.
|
|
|
|
See `Explorer.Chain.Import.all/1` for options and returns.
|
|
"""
|
|
@spec import(Import.all_options()) :: Import.all_result()
|
|
def import(options) do
|
|
Import.all(options)
|
|
end
|
|
|
|
@doc """
|
|
The percentage of indexed blocks on the chain.
|
|
|
|
If there are no blocks, the percentage is 0.
|
|
|
|
iex> Explorer.Chain.indexed_ratio_blocks()
|
|
Decimal.new(0)
|
|
|
|
"""
|
|
@spec indexed_ratio_blocks() :: Decimal.t()
|
|
def indexed_ratio_blocks do
|
|
if Application.get_env(:indexer, Indexer.Supervisor)[:enabled] do
|
|
%{min: min_saved_block_number, max: max_saved_block_number} = BlockNumber.get_all()
|
|
|
|
min_blockchain_block_number = min_block_number_from_config(:first_block)
|
|
|
|
case {min_saved_block_number, max_saved_block_number} do
|
|
{0, 0} ->
|
|
Decimal.new(0)
|
|
|
|
_ ->
|
|
BlockCache.estimated_count()
|
|
|> Decimal.div(max_saved_block_number - min_blockchain_block_number + 1)
|
|
|> (&if(
|
|
greater_or_equal_0_99(&1) &&
|
|
min_saved_block_number <= min_blockchain_block_number,
|
|
do: Decimal.new(1),
|
|
else: &1
|
|
)).()
|
|
|> format_indexed_ratio()
|
|
end
|
|
else
|
|
Decimal.new(1)
|
|
end
|
|
end
|
|
|
|
@spec indexed_ratio_internal_transactions() :: Decimal.t()
|
|
def indexed_ratio_internal_transactions do
|
|
if Application.get_env(:indexer, Indexer.Supervisor)[:enabled] &&
|
|
not Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor)[:disabled?] do
|
|
%{max: max_saved_block_number} = BlockNumber.get_all()
|
|
pbo_count = PendingBlockOperationCache.estimated_count()
|
|
|
|
min_blockchain_trace_block_number = min_block_number_from_config(:trace_first_block)
|
|
|
|
case max_saved_block_number do
|
|
0 ->
|
|
Decimal.new(0)
|
|
|
|
_ ->
|
|
full_blocks_range = max_saved_block_number - min_blockchain_trace_block_number + 1
|
|
processed_int_txs_for_blocks_count = full_blocks_range - pbo_count
|
|
|
|
processed_int_txs_for_blocks_count
|
|
|> Decimal.div(full_blocks_range)
|
|
|> (&if(
|
|
greater_or_equal_0_99(&1),
|
|
do: Decimal.new(1),
|
|
else: &1
|
|
)).()
|
|
|> format_indexed_ratio()
|
|
end
|
|
else
|
|
Decimal.new(1)
|
|
end
|
|
end
|
|
|
|
@spec greater_or_equal_0_99(Decimal.t()) :: boolean()
|
|
defp greater_or_equal_0_99(value) do
|
|
Decimal.compare(value, Decimal.from_float(0.99)) == :gt ||
|
|
Decimal.compare(value, Decimal.from_float(0.99)) == :eq
|
|
end
|
|
|
|
@spec min_block_number_from_config(atom()) :: integer()
|
|
defp min_block_number_from_config(block_type) do
|
|
case Integer.parse(Application.get_env(:indexer, block_type)) do
|
|
{block_number, _} -> block_number
|
|
_ -> 0
|
|
end
|
|
end
|
|
|
|
@spec format_indexed_ratio(Decimal.t()) :: Decimal.t()
|
|
defp format_indexed_ratio(raw_ratio) do
|
|
raw_ratio
|
|
|> Decimal.round(2, :down)
|
|
|> Decimal.min(Decimal.new(1))
|
|
end
|
|
|
|
@spec fetch_min_block_number() :: non_neg_integer
|
|
def fetch_min_block_number do
|
|
query =
|
|
from(block in Block,
|
|
select: block.number,
|
|
where: block.consensus == true,
|
|
order_by: [asc: block.number],
|
|
limit: 1
|
|
)
|
|
|
|
Repo.one(query) || 0
|
|
rescue
|
|
_ ->
|
|
0
|
|
end
|
|
|
|
@spec fetch_max_block_number() :: non_neg_integer
|
|
def fetch_max_block_number do
|
|
query =
|
|
from(block in Block,
|
|
select: block.number,
|
|
where: block.consensus == true,
|
|
order_by: [desc: block.number],
|
|
limit: 1
|
|
)
|
|
|
|
Repo.one(query) || 0
|
|
rescue
|
|
_ ->
|
|
0
|
|
end
|
|
|
|
def fetch_block_by_hash(block_hash) do
|
|
Repo.get(Block, block_hash)
|
|
end
|
|
|
|
def filter_consensus_block_numbers(block_numbers) do
|
|
query =
|
|
from(
|
|
block in Block,
|
|
where: block.number in ^block_numbers,
|
|
where: block.consensus == true,
|
|
select: block.number
|
|
)
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
The number of `t:Explorer.Chain.InternalTransaction.t/0`.
|
|
|
|
iex> transaction = :transaction |> insert() |> with_block()
|
|
iex> insert(:internal_transaction, index: 0, transaction: transaction, block_hash: transaction.block_hash, block_index: 0)
|
|
iex> Explorer.Chain.internal_transaction_count()
|
|
1
|
|
|
|
If there are none, the count is `0`.
|
|
|
|
iex> Explorer.Chain.internal_transaction_count()
|
|
0
|
|
|
|
"""
|
|
def internal_transaction_count do
|
|
Repo.aggregate(InternalTransaction.where_nonpending_block(), :count, :transaction_hash)
|
|
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.Block.t/0` will not be included in the page `entries`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{block_number}`). Results will be the internal
|
|
transactions older than the `block_number` that are passed.
|
|
* ':block_type' - use to filter by type of block; Uncle`, `Reorg`, or `Block` (default).
|
|
|
|
"""
|
|
@spec list_blocks([paging_options | necessity_by_association_option | api?]) :: [Block.t()]
|
|
def list_blocks(options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options) || @default_paging_options
|
|
block_type = Keyword.get(options, :block_type, "Block")
|
|
|
|
cond do
|
|
block_type == "Block" && !paging_options.key ->
|
|
block_from_cache(block_type, paging_options, necessity_by_association, options)
|
|
|
|
block_type == "Uncle" && !paging_options.key ->
|
|
uncles_from_cache(block_type, paging_options, necessity_by_association, options)
|
|
|
|
true ->
|
|
fetch_blocks(block_type, paging_options, necessity_by_association, options)
|
|
end
|
|
end
|
|
|
|
defp block_from_cache(block_type, paging_options, necessity_by_association, options) do
|
|
case Blocks.take_enough(paging_options.page_size) do
|
|
nil ->
|
|
elements = fetch_blocks(block_type, paging_options, necessity_by_association, options)
|
|
|
|
Blocks.update(elements)
|
|
|
|
elements
|
|
|
|
blocks ->
|
|
blocks
|
|
end
|
|
end
|
|
|
|
def uncles_from_cache(block_type, paging_options, necessity_by_association, options) do
|
|
case Uncles.take_enough(paging_options.page_size) do
|
|
nil ->
|
|
elements = fetch_blocks(block_type, paging_options, necessity_by_association, options)
|
|
|
|
Uncles.update(elements)
|
|
|
|
elements
|
|
|
|
blocks ->
|
|
blocks
|
|
end
|
|
end
|
|
|
|
defp fetch_blocks(block_type, paging_options, necessity_by_association, options) do
|
|
Block
|
|
|> Block.block_type_filter(block_type)
|
|
|> page_blocks(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by(desc: :number)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Map `block_number`s to their `t:Explorer.Chain.Block.t/0` `hash` `t:Explorer.Chain.Hash.Full.t/0`.
|
|
|
|
Does not include non-consensus blocks.
|
|
|
|
iex> block = insert(:block, consensus: false)
|
|
iex> Explorer.Chain.block_hash_by_number([block.number])
|
|
%{}
|
|
|
|
"""
|
|
@spec block_hash_by_number([Block.block_number()]) :: %{Block.block_number() => Hash.Full.t()}
|
|
def block_hash_by_number(block_numbers) when is_list(block_numbers) do
|
|
query =
|
|
from(block in Block,
|
|
where: block.consensus == true and block.number in ^block_numbers,
|
|
select: {block.number, block.hash}
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> Enum.into(%{})
|
|
end
|
|
|
|
@doc """
|
|
Lists the top `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance and address hash.
|
|
|
|
"""
|
|
@spec list_top_addresses :: [{Address.t(), non_neg_integer()}]
|
|
def list_top_addresses(options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
if is_nil(paging_options.key) do
|
|
paging_options.page_size
|
|
|> Accounts.take_enough()
|
|
|> case do
|
|
nil ->
|
|
get_addresses(options)
|
|
|
|
accounts ->
|
|
Enum.map(
|
|
accounts,
|
|
&{&1,
|
|
if is_nil(&1.nonce) do
|
|
0
|
|
else
|
|
&1.nonce + 1
|
|
end}
|
|
)
|
|
end
|
|
else
|
|
fetch_top_addresses(options)
|
|
end
|
|
end
|
|
|
|
defp get_addresses(options) do
|
|
accounts_with_n = fetch_top_addresses(options)
|
|
|
|
accounts_with_n
|
|
|> Enum.map(fn {address, _n} -> address end)
|
|
|> Accounts.update()
|
|
|
|
accounts_with_n
|
|
end
|
|
|
|
defp fetch_top_addresses(options) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
base_query =
|
|
from(a in Address,
|
|
where: a.fetched_coin_balance > ^0,
|
|
order_by: [desc: a.fetched_coin_balance, asc: a.hash],
|
|
preload: [:names],
|
|
select: {a, fragment("coalesce(1 + ?, 0)", a.nonce)}
|
|
)
|
|
|
|
base_query
|
|
|> page_addresses(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Lists the top `t:Explorer.Chain.Token.t/0`'s'.
|
|
|
|
"""
|
|
@spec list_top_tokens(String.t()) :: [{Token.t(), non_neg_integer()}]
|
|
def list_top_tokens(filter, options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
token_type = Keyword.get(options, :token_type, nil)
|
|
sorting = Keyword.get(options, :sorting, [])
|
|
|
|
fetch_top_tokens(filter, paging_options, token_type, sorting, options)
|
|
end
|
|
|
|
defp fetch_top_tokens(filter, paging_options, token_type, sorting, options) do
|
|
base_query = Token.base_token_query(token_type, sorting)
|
|
|
|
base_query_with_paging =
|
|
base_query
|
|
|> Token.page_tokens(paging_options, sorting)
|
|
|> limit(^paging_options.page_size)
|
|
|
|
query =
|
|
if filter && filter !== "" do
|
|
case Search.prepare_search_term(filter) do
|
|
{:some, filter_term} ->
|
|
base_query_with_paging
|
|
|> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term))
|
|
|
|
_ ->
|
|
base_query_with_paging
|
|
end
|
|
else
|
|
base_query_with_paging
|
|
end
|
|
|
|
query
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`.
|
|
"""
|
|
def stream_blocks_without_rewards(initial, reducer, limited? \\ false) when is_function(reducer, 2) do
|
|
Block.blocks_without_reward_query()
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
Finds all transactions of a certain block number
|
|
"""
|
|
def get_transactions_of_block_number(block_number) do
|
|
block_number
|
|
|> Transaction.transactions_with_block_number()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Finds all Blocks validated by the address with the given 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.Block.t/0` has no associated record for that association, then the
|
|
`t:Explorer.Chain.Block.t/0` will not be included in the page `entries`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{block_number}`) and. Results will be the internal
|
|
transactions older than the `block_number` that are passed.
|
|
|
|
Returns all blocks validated by the address given.
|
|
"""
|
|
@spec get_blocks_validated_by_address(
|
|
[paging_options | necessity_by_association_option],
|
|
Hash.Address.t()
|
|
) :: [Block.t()]
|
|
def get_blocks_validated_by_address(options \\ [], address_hash) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
Block
|
|
|> join_associations(necessity_by_association)
|
|
|> where(miner_hash: ^address_hash)
|
|
|> page_blocks(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by(desc: :number)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Counts all of the block validations and groups by the `miner_hash`.
|
|
"""
|
|
def each_address_block_validation_count(fun) when is_function(fun, 1) do
|
|
query =
|
|
from(
|
|
b in Block,
|
|
join: addr in Address,
|
|
on: b.miner_hash == addr.hash,
|
|
select: {b.miner_hash, count(b.miner_hash)},
|
|
group_by: b.miner_hash
|
|
)
|
|
|
|
Repo.stream_each(query, fun)
|
|
end
|
|
|
|
@doc """
|
|
Return the balance in usd corresponding to this token. Return nil if the fiat_value of the token is not present.
|
|
"""
|
|
def balance_in_fiat(%{fiat_value: fiat_value} = token_balance) when not is_nil(fiat_value) do
|
|
token_balance.fiat_value
|
|
end
|
|
|
|
def balance_in_fiat(%{token: %{fiat_value: fiat_value, decimals: decimals}}) when nil in [fiat_value, decimals] do
|
|
nil
|
|
end
|
|
|
|
def balance_in_fiat(%{token: %{fiat_value: fiat_value, decimals: decimals}} = token_balance) do
|
|
tokens = CurrencyHelper.divide_decimals(token_balance.value, decimals)
|
|
Decimal.mult(tokens, fiat_value)
|
|
end
|
|
|
|
def contract?(%{contract_code: nil}), do: false
|
|
|
|
def contract?(%{contract_code: _}), do: true
|
|
|
|
@doc """
|
|
Returns a stream of unfetched `t:Explorer.Chain.Address.CoinBalance.t/0`.
|
|
|
|
When there are addresses, the `reducer` is called for each `t:Explorer.Chain.Address.t/0` `hash` and all
|
|
`t:Explorer.Chain.Block.t/0` `block_number` that address is mentioned.
|
|
|
|
| Address Hash Schema | Address Hash Field | Block Number Schema | Block Number Field |
|
|
|--------------------------------------------|---------------------------------|------------------------------------|--------------------|
|
|
| `t:Explorer.Chain.Block.t/0` | `miner_hash` | `t:Explorer.Chain.Block.t/0` | `number` |
|
|
| `t:Explorer.Chain.Transaction.t/0` | `from_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` |
|
|
| `t:Explorer.Chain.Transaction.t/0` | `to_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` |
|
|
| `t:Explorer.Chain.Log.t/0` | `address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` |
|
|
| `t:Explorer.Chain.InternalTransaction.t/0` | `created_contract_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` |
|
|
| `t:Explorer.Chain.InternalTransaction.t/0` | `from_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` |
|
|
| `t:Explorer.Chain.InternalTransaction.t/0` | `to_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` |
|
|
|
|
Pending `t:Explorer.Chain.Transaction.t/0` `from_address_hash` and `to_address_hash` aren't returned because they
|
|
don't have an associated block number.
|
|
|
|
When there are no addresses, the `reducer` is never called and the `initial` is returned in an `:ok` tuple.
|
|
|
|
When an `t:Explorer.Chain.Address.t/0` `hash` is used multiple times, all unique `t:Explorer.Chain.Block.t/0` `number`
|
|
will be returned.
|
|
"""
|
|
@spec stream_unfetched_balances(
|
|
initial :: accumulator,
|
|
reducer ::
|
|
(entry :: %{address_hash: Hash.Address.t(), block_number: Block.block_number()}, accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_unfetched_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do
|
|
query =
|
|
from(
|
|
balance in CoinBalance,
|
|
where: is_nil(balance.value_fetched_at),
|
|
select: %{address_hash: balance.address_hash, block_number: balance.block_number}
|
|
)
|
|
|
|
query
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
Returns a stream of all token balances that weren't fetched values.
|
|
"""
|
|
@spec stream_unfetched_token_balances(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: TokenBalance.t(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_unfetched_token_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do
|
|
TokenBalance.unfetched_token_balances()
|
|
|> add_token_balances_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
Returns a stream of all blocks with unfetched internal transactions, using
|
|
the `pending_block_operation` table.
|
|
|
|
iex> unfetched = insert(:block)
|
|
iex> insert(:pending_block_operation, block: unfetched, block_number: unfetched.number)
|
|
iex> {:ok, number_set} = Explorer.Chain.stream_blocks_with_unfetched_internal_transactions(
|
|
...> MapSet.new(),
|
|
...> fn number, acc ->
|
|
...> MapSet.put(acc, number)
|
|
...> end
|
|
...> )
|
|
iex> unfetched.number in number_set
|
|
true
|
|
|
|
"""
|
|
@spec stream_blocks_with_unfetched_internal_transactions(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: term(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_blocks_with_unfetched_internal_transactions(initial, reducer, limited? \\ false)
|
|
when is_function(reducer, 2) do
|
|
query =
|
|
from(
|
|
po in PendingBlockOperation,
|
|
where: not is_nil(po.block_number),
|
|
select: po.block_number
|
|
)
|
|
|
|
query
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
def remove_nonconsensus_blocks_from_pending_ops(block_hashes) do
|
|
query =
|
|
from(
|
|
po in PendingBlockOperation,
|
|
where: po.block_hash in ^block_hashes
|
|
)
|
|
|
|
{_, _} = Repo.delete_all(query)
|
|
|
|
:ok
|
|
end
|
|
|
|
def remove_nonconsensus_blocks_from_pending_ops do
|
|
query =
|
|
from(
|
|
po in PendingBlockOperation,
|
|
inner_join: block in Block,
|
|
on: block.hash == po.block_hash,
|
|
where: block.consensus == false
|
|
)
|
|
|
|
{_, _} = Repo.delete_all(query)
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec stream_transactions_with_unfetched_created_contract_codes(
|
|
fields :: [
|
|
:block_hash
|
|
| :created_contract_code_indexed_at
|
|
| :from_address_hash
|
|
| :gas
|
|
| :gas_price
|
|
| :hash
|
|
| :index
|
|
| :input
|
|
| :nonce
|
|
| :r
|
|
| :s
|
|
| :to_address_hash
|
|
| :v
|
|
| :value
|
|
],
|
|
initial :: accumulator,
|
|
reducer :: (entry :: term(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_transactions_with_unfetched_created_contract_codes(fields, initial, reducer, limited? \\ false)
|
|
when is_function(reducer, 2) do
|
|
query =
|
|
from(t in Transaction,
|
|
where:
|
|
not is_nil(t.block_hash) and not is_nil(t.created_contract_address_hash) and
|
|
is_nil(t.created_contract_code_indexed_at),
|
|
select: ^fields
|
|
)
|
|
|
|
query
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@spec stream_mined_transactions(
|
|
fields :: [
|
|
:block_hash
|
|
| :created_contract_code_indexed_at
|
|
| :from_address_hash
|
|
| :gas
|
|
| :gas_price
|
|
| :hash
|
|
| :index
|
|
| :input
|
|
| :nonce
|
|
| :r
|
|
| :s
|
|
| :to_address_hash
|
|
| :v
|
|
| :value
|
|
],
|
|
initial :: accumulator,
|
|
reducer :: (entry :: term(), accumulator -> accumulator)
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_mined_transactions(fields, initial, reducer) when is_function(reducer, 2) do
|
|
query =
|
|
from(t in Transaction,
|
|
where: not is_nil(t.block_hash) and not is_nil(t.nonce) and not is_nil(t.from_address_hash),
|
|
select: ^fields
|
|
)
|
|
|
|
Repo.stream_reduce(query, initial, reducer)
|
|
end
|
|
|
|
@spec stream_pending_transactions(
|
|
fields :: [
|
|
:block_hash
|
|
| :created_contract_code_indexed_at
|
|
| :from_address_hash
|
|
| :gas
|
|
| :gas_price
|
|
| :hash
|
|
| :index
|
|
| :input
|
|
| :nonce
|
|
| :r
|
|
| :s
|
|
| :to_address_hash
|
|
| :v
|
|
| :value
|
|
],
|
|
initial :: accumulator,
|
|
reducer :: (entry :: term(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_pending_transactions(fields, initial, reducer, limited? \\ false) when is_function(reducer, 2) do
|
|
query =
|
|
Transaction
|
|
|> pending_transactions_query()
|
|
|> select(^fields)
|
|
|> add_fetcher_limit(limited?)
|
|
|
|
Repo.stream_reduce(query, initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
Returns a stream of all blocks that are marked as unfetched in `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`.
|
|
For each uncle block a `hash` of nephew block and an `index` of the block in it are returned.
|
|
|
|
When a block is fetched, its uncles are transformed into `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` and can be
|
|
returned. Once the uncle is imported its corresponding `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`
|
|
`uncle_fetched_at` will be set and it won't be returned anymore.
|
|
"""
|
|
@spec stream_unfetched_uncles(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: term(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_unfetched_uncles(initial, reducer, limited? \\ false) when is_function(reducer, 2) do
|
|
query =
|
|
from(bsdr in Block.SecondDegreeRelation,
|
|
where: is_nil(bsdr.uncle_fetched_at) and not is_nil(bsdr.index),
|
|
select: [:nephew_hash, :index]
|
|
)
|
|
|
|
query
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
The number of `t:Explorer.Chain.Log.t/0`.
|
|
|
|
iex> transaction = :transaction |> insert() |> with_block()
|
|
iex> insert(:log, transaction: transaction, index: 0)
|
|
iex> Explorer.Chain.log_count()
|
|
1
|
|
|
|
When there are no `t:Explorer.Chain.Log.t/0`.
|
|
|
|
iex> Explorer.Chain.log_count()
|
|
0
|
|
|
|
"""
|
|
def log_count do
|
|
Repo.one!(from(log in "logs", select: fragment("COUNT(*)")))
|
|
end
|
|
|
|
@doc """
|
|
Max consensus block numbers.
|
|
|
|
If blocks are skipped and inserted out of number order, the max number is still returned
|
|
|
|
iex> insert(:block, number: 2)
|
|
iex> insert(:block, number: 1)
|
|
iex> Explorer.Chain.max_consensus_block_number()
|
|
{:ok, 2}
|
|
|
|
Non-consensus blocks are ignored
|
|
|
|
iex> insert(:block, number: 3, consensus: false)
|
|
iex> insert(:block, number: 2, consensus: true)
|
|
iex> Explorer.Chain.max_consensus_block_number()
|
|
{:ok, 2}
|
|
|
|
If there are no blocks, `{:error, :not_found}` is returned
|
|
|
|
iex> Explorer.Chain.max_consensus_block_number()
|
|
{:error, :not_found}
|
|
|
|
"""
|
|
@spec max_consensus_block_number() :: {:ok, Block.block_number()} | {:error, :not_found}
|
|
def max_consensus_block_number do
|
|
Block
|
|
|> where(consensus: true)
|
|
|> Repo.aggregate(:max, :number)
|
|
|> case do
|
|
nil -> {:error, :not_found}
|
|
number -> {:ok, number}
|
|
end
|
|
end
|
|
|
|
@spec block_height() :: block_height()
|
|
def block_height(options \\ []) do
|
|
query = from(block in Block, select: coalesce(max(block.number), 0), where: block.consensus == true)
|
|
|
|
select_repo(options).one!(query)
|
|
end
|
|
|
|
def last_db_block_status do
|
|
query =
|
|
from(block in Block,
|
|
select: {block.number, block.timestamp},
|
|
where: block.consensus == true,
|
|
order_by: [desc: block.number],
|
|
limit: 1
|
|
)
|
|
|
|
query
|
|
|> Repo.one()
|
|
|> block_status()
|
|
end
|
|
|
|
def last_cache_block_status do
|
|
[
|
|
paging_options: %PagingOptions{page_size: 1}
|
|
]
|
|
|> list_blocks()
|
|
|> List.last()
|
|
|> case do
|
|
%{timestamp: timestamp, number: number} ->
|
|
block_status({number, timestamp})
|
|
|
|
_ ->
|
|
block_status(nil)
|
|
end
|
|
end
|
|
|
|
@spec upsert_last_fetched_counter(map()) :: {:ok, LastFetchedCounter.t()} | {:error, Ecto.Changeset.t()}
|
|
def upsert_last_fetched_counter(params) do
|
|
changeset = LastFetchedCounter.changeset(%LastFetchedCounter{}, params)
|
|
|
|
Repo.insert(changeset,
|
|
on_conflict: :replace_all,
|
|
conflict_target: [:counter_type]
|
|
)
|
|
end
|
|
|
|
def get_last_fetched_counter(type, options \\ []) do
|
|
query =
|
|
from(
|
|
last_fetched_counter in LastFetchedCounter,
|
|
where: last_fetched_counter.counter_type == ^type,
|
|
select: last_fetched_counter.value
|
|
)
|
|
|
|
select_repo(options).one(query) || Decimal.new(0)
|
|
end
|
|
|
|
defp block_status({number, timestamp}) do
|
|
now = DateTime.utc_now()
|
|
last_block_period = DateTime.diff(now, timestamp, :millisecond)
|
|
|
|
if last_block_period > Application.get_env(:explorer, :healthy_blocks_period) do
|
|
{:error, number, timestamp}
|
|
else
|
|
{:ok, number, timestamp}
|
|
end
|
|
end
|
|
|
|
defp block_status(nil), do: {:error, :no_blocks}
|
|
|
|
def fetch_min_missing_block_cache(from \\ nil, to \\ nil) do
|
|
from_block_number = from || 0
|
|
to_block_number = to || BlockNumber.get_max()
|
|
|
|
if to_block_number > 0 do
|
|
query =
|
|
from(b in Block,
|
|
right_join:
|
|
missing_range in fragment(
|
|
"""
|
|
(SELECT b1.number
|
|
FROM generate_series((?)::integer, (?)::integer) AS b1(number)
|
|
WHERE NOT EXISTS
|
|
(SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus))
|
|
""",
|
|
^from_block_number,
|
|
^to_block_number
|
|
),
|
|
on: b.number == missing_range.number,
|
|
select: min(missing_range.number)
|
|
)
|
|
|
|
Repo.one(query, timeout: :infinity)
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Calculates the ranges of missing consensus blocks in `range`.
|
|
|
|
When there are no blocks, the entire range is missing.
|
|
|
|
iex> Explorer.Chain.missing_block_number_ranges(0..5)
|
|
[0..5]
|
|
|
|
If the block numbers from `0` to `max_block_number/0` are contiguous, then no block numbers are missing
|
|
|
|
iex> insert(:block, number: 0)
|
|
iex> insert(:block, number: 1)
|
|
iex> Explorer.Chain.missing_block_number_ranges(0..1)
|
|
[]
|
|
|
|
If there are gaps between the `first` and `last` of `range`, then the missing numbers are compacted into ranges.
|
|
Single missing numbers become ranges with the single number as the start and end.
|
|
|
|
iex> insert(:block, number: 0)
|
|
iex> insert(:block, number: 2)
|
|
iex> insert(:block, number: 5)
|
|
iex> Explorer.Chain.missing_block_number_ranges(0..5)
|
|
[1..1, 3..4]
|
|
|
|
Flipping the order of `first` and `last` in the `range` flips the order that the missing ranges are returned. This
|
|
allows `missing_block_numbers` to be used to generate the sequence down or up from a starting block number.
|
|
|
|
iex> insert(:block, number: 0)
|
|
iex> insert(:block, number: 2)
|
|
iex> insert(:block, number: 5)
|
|
iex> Explorer.Chain.missing_block_number_ranges(5..0)
|
|
[4..3, 1..1]
|
|
|
|
If only non-consensus blocks exist for a number, the number still counts as missing.
|
|
|
|
iex> insert(:block, number: 0)
|
|
iex> insert(:block, number: 1, consensus: false)
|
|
iex> insert(:block, number: 2)
|
|
iex> Explorer.Chain.missing_block_number_ranges(2..0)
|
|
[1..1]
|
|
|
|
if range starts with non-consensus block in the middle of the chain, it returns missing numbers.
|
|
|
|
iex> insert(:block, number: 12859383, consensus: true)
|
|
iex> insert(:block, number: 12859384, consensus: false)
|
|
iex> insert(:block, number: 12859386, consensus: true)
|
|
iex> Explorer.Chain.missing_block_number_ranges(12859384..12859385)
|
|
[12859384..12859385]
|
|
|
|
if range starts with missing block in the middle of the chain, it returns missing numbers.
|
|
|
|
iex> insert(:block, number: 12859383, consensus: true)
|
|
iex> insert(:block, number: 12859386, consensus: true)
|
|
iex> Explorer.Chain.missing_block_number_ranges(12859384..12859385)
|
|
[12859384..12859385]
|
|
|
|
"""
|
|
@spec missing_block_number_ranges(Range.t()) :: [Range.t()]
|
|
def missing_block_number_ranges(range)
|
|
|
|
def missing_block_number_ranges(range_start..range_end) do
|
|
range_min = min(range_start, range_end)
|
|
range_max = max(range_start, range_end)
|
|
|
|
ordered_missing_query =
|
|
from(b in Block,
|
|
right_join:
|
|
missing_range in fragment(
|
|
"""
|
|
(
|
|
SELECT distinct b1.number
|
|
FROM generate_series((?)::integer, (?)::integer) AS b1(number)
|
|
WHERE NOT EXISTS
|
|
(SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus)
|
|
ORDER BY b1.number DESC
|
|
)
|
|
""",
|
|
^range_min,
|
|
^range_max
|
|
),
|
|
on: b.number == missing_range.number,
|
|
select: missing_range.number,
|
|
order_by: missing_range.number,
|
|
distinct: missing_range.number
|
|
)
|
|
|
|
missing_blocks = Repo.all(ordered_missing_query, timeout: :infinity)
|
|
|
|
[block_ranges, last_block_range_start, last_block_range_end] =
|
|
missing_blocks
|
|
|> Enum.reduce([[], nil, nil], fn block_number, [block_ranges, last_block_range_start, last_block_range_end] ->
|
|
cond do
|
|
!last_block_range_start ->
|
|
[block_ranges, block_number, block_number]
|
|
|
|
block_number == last_block_range_end + 1 ->
|
|
[block_ranges, last_block_range_start, block_number]
|
|
|
|
true ->
|
|
block_ranges = block_ranges_extend(block_ranges, last_block_range_start, last_block_range_end)
|
|
[block_ranges, block_number, block_number]
|
|
end
|
|
end)
|
|
|
|
final_block_ranges =
|
|
if last_block_range_start && last_block_range_end do
|
|
block_ranges_extend(block_ranges, last_block_range_start, last_block_range_end)
|
|
else
|
|
block_ranges
|
|
end
|
|
|
|
ordered_block_ranges =
|
|
final_block_ranges
|
|
|> Enum.sort(fn %Range{first: first1, last: _}, %Range{first: first2, last: _} ->
|
|
if range_start <= range_end, do: first1 <= first2, else: first1 >= first2
|
|
end)
|
|
|> Enum.map(fn %Range{first: first, last: last} = range ->
|
|
if range_start <= range_end do
|
|
range
|
|
else
|
|
set_new_range(last, first)
|
|
end
|
|
end)
|
|
|
|
ordered_block_ranges
|
|
end
|
|
|
|
defp set_new_range(last, first) do
|
|
if last > first, do: set_range(last, first, -1), else: set_range(last, first, 1)
|
|
end
|
|
|
|
defp set_range(last, first, step) do
|
|
%Range{first: last, last: first, step: step}
|
|
end
|
|
|
|
defp block_ranges_extend(block_ranges, block_range_start, block_range_end) do
|
|
# credo:disable-for-next-line
|
|
block_ranges ++ [Range.new(block_range_start, block_range_end)]
|
|
end
|
|
|
|
@doc """
|
|
Finds consensus `t:Explorer.Chain.Block.t/0` with `number`.
|
|
|
|
## 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.Block.t/0` will not be included in the page `entries`.
|
|
|
|
"""
|
|
@spec number_to_block(Block.block_number(), [necessity_by_association_option | api?]) ::
|
|
{:ok, Block.t()} | {:error, :not_found}
|
|
def number_to_block(number, options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
Block
|
|
|> where(consensus: true, number: ^number)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).one()
|
|
|> case do
|
|
nil -> {:error, :not_found}
|
|
block -> {:ok, block}
|
|
end
|
|
end
|
|
|
|
@spec nonconsensus_block_by_number(Block.block_number(), [api?]) :: {:ok, Block.t()} | {:error, :not_found}
|
|
def nonconsensus_block_by_number(number, options) do
|
|
Block
|
|
|> where(consensus: false, number: ^number)
|
|
|> select_repo(options).one()
|
|
|> case do
|
|
nil -> {:error, :not_found}
|
|
block -> {:ok, block}
|
|
end
|
|
end
|
|
|
|
@spec timestamp_to_block_number(DateTime.t(), :before | :after, boolean()) ::
|
|
{:ok, Block.block_number()} | {:error, :not_found}
|
|
def timestamp_to_block_number(given_timestamp, closest, from_api) do
|
|
{:ok, t} = Timex.format(given_timestamp, "%Y-%m-%d %H:%M:%S", :strftime)
|
|
|
|
inner_query =
|
|
from(
|
|
block in Block,
|
|
where: block.consensus == true,
|
|
where:
|
|
fragment("? <= TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS') + (1 * interval '1 minute')", block.timestamp, ^t),
|
|
where:
|
|
fragment("? >= TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS') - (1 * interval '1 minute')", block.timestamp, ^t)
|
|
)
|
|
|
|
query =
|
|
from(
|
|
block in subquery(inner_query),
|
|
select: block,
|
|
order_by:
|
|
fragment("abs(extract(epoch from (? - TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS'))))", block.timestamp, ^t),
|
|
limit: 1
|
|
)
|
|
|
|
repo = if from_api, do: Repo.replica(), else: Repo
|
|
|
|
query
|
|
|> repo.one(timeout: :infinity)
|
|
|> case do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%{:number => number, :timestamp => timestamp} ->
|
|
block_number = get_block_number_based_on_closest(closest, timestamp, given_timestamp, number)
|
|
|
|
{:ok, block_number}
|
|
end
|
|
end
|
|
|
|
defp get_block_number_based_on_closest(closest, timestamp, given_timestamp, number) do
|
|
case closest do
|
|
:before ->
|
|
if DateTime.compare(timestamp, given_timestamp) == :lt ||
|
|
DateTime.compare(timestamp, given_timestamp) == :eq do
|
|
number
|
|
else
|
|
number - 1
|
|
end
|
|
|
|
:after ->
|
|
if DateTime.compare(timestamp, given_timestamp) == :lt ||
|
|
DateTime.compare(timestamp, given_timestamp) == :eq do
|
|
number + 1
|
|
else
|
|
number
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Count of pending `t:Explorer.Chain.Transaction.t/0`.
|
|
|
|
A count of all pending transactions.
|
|
|
|
iex> insert(:transaction)
|
|
iex> :transaction |> insert() |> with_block()
|
|
iex> Explorer.Chain.pending_transaction_count()
|
|
1
|
|
|
|
"""
|
|
@spec pending_transaction_count() :: non_neg_integer()
|
|
def pending_transaction_count do
|
|
Transaction
|
|
|> pending_transactions_query()
|
|
|> Repo.aggregate(:count, :hash)
|
|
end
|
|
|
|
@doc """
|
|
Returns the paged list of collated transactions that occurred recently from newest to oldest using `block_number`
|
|
and `index`.
|
|
|
|
iex> newest_first_transactions = 50 |> insert_list(:transaction) |> with_block() |> Enum.reverse()
|
|
iex> oldest_seen = Enum.at(newest_first_transactions, 9)
|
|
iex> paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}}
|
|
iex> recent_collated_transactions = Explorer.Chain.recent_collated_transactions(true, paging_options: paging_options)
|
|
iex> length(recent_collated_transactions)
|
|
10
|
|
iex> hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash
|
|
true
|
|
|
|
## 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 list.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{block_number, index}`) and. Results will be the transactions older than
|
|
the `block_number` and `index` that are passed.
|
|
|
|
"""
|
|
@spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option | api?]) :: [
|
|
Transaction.t()
|
|
]
|
|
def recent_collated_transactions(old_ui?, options \\ [])
|
|
when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
method_id_filter = Keyword.get(options, :method)
|
|
type_filter = Keyword.get(options, :type)
|
|
|
|
fetch_recent_collated_transactions(
|
|
old_ui?,
|
|
paging_options,
|
|
necessity_by_association,
|
|
method_id_filter,
|
|
type_filter,
|
|
options
|
|
)
|
|
end
|
|
|
|
# RAP - random access pagination
|
|
@spec recent_collated_transactions_for_rap([paging_options | necessity_by_association_option]) :: %{
|
|
:total_transactions_count => non_neg_integer(),
|
|
:transactions => [Transaction.t()]
|
|
}
|
|
def recent_collated_transactions_for_rap(options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
total_transactions_count = transactions_available_count()
|
|
|
|
fetched_transactions =
|
|
if is_nil(paging_options.key) or paging_options.page_number == 1 do
|
|
paging_options.page_size
|
|
|> Kernel.+(1)
|
|
|> Transactions.take_enough()
|
|
|> case do
|
|
nil ->
|
|
transactions = fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association)
|
|
Transactions.update(transactions)
|
|
transactions
|
|
|
|
transactions ->
|
|
transactions
|
|
end
|
|
else
|
|
fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association)
|
|
end
|
|
|
|
%{total_transactions_count: total_transactions_count, transactions: fetched_transactions}
|
|
end
|
|
|
|
def default_page_size, do: @default_page_size
|
|
|
|
def fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) do
|
|
fetch_transactions_for_rap()
|
|
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
|
|
|> handle_random_access_paging_options(paging_options)
|
|
|> join_associations(necessity_by_association)
|
|
|> preload([{:token_transfers, [:token, :from_address, :to_address]}])
|
|
|> Repo.all()
|
|
end
|
|
|
|
defp fetch_transactions_for_rap do
|
|
Transaction
|
|
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
|
|
end
|
|
|
|
def transactions_available_count do
|
|
Transaction
|
|
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
|
|
|> limit(^@limit_showing_transactions)
|
|
|> Repo.aggregate(:count, :hash)
|
|
end
|
|
|
|
def fetch_recent_collated_transactions(
|
|
old_ui?,
|
|
paging_options,
|
|
necessity_by_association,
|
|
method_id_filter,
|
|
type_filter,
|
|
options
|
|
) do
|
|
paging_options
|
|
|> fetch_transactions()
|
|
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
|
|
|> apply_filter_by_method_id_to_transactions(method_id_filter)
|
|
|> apply_filter_by_tx_type_to_transactions(type_filter)
|
|
|> join_associations(necessity_by_association)
|
|
|> put_has_token_transfers_to_tx(old_ui?)
|
|
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|
|
|> select_repo(options).all()
|
|
|> (&if(old_ui?,
|
|
do: &1,
|
|
else:
|
|
Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_necessity_by_association, options) end)
|
|
)).()
|
|
end
|
|
|
|
@doc """
|
|
Return the list of pending transactions that occurred recently.
|
|
|
|
iex> 2 |> insert_list(:transaction)
|
|
iex> :transaction |> insert() |> with_block()
|
|
iex> 8 |> insert_list(:transaction)
|
|
iex> recent_pending_transactions = Explorer.Chain.recent_pending_transactions()
|
|
iex> length(recent_pending_transactions)
|
|
10
|
|
iex> Enum.all?(recent_pending_transactions, fn %Explorer.Chain.Transaction{block_hash: block_hash} ->
|
|
...> is_nil(block_hash)
|
|
...> end)
|
|
true
|
|
|
|
## 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 list.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` (defaults to
|
|
`#{@default_paging_options.page_size}`) and `:key` (a tuple of the lowest/oldest `{inserted_at, hash}`) and.
|
|
Results will be the transactions older than the `inserted_at` and `hash` that are passed.
|
|
|
|
"""
|
|
@spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false) :: [
|
|
Transaction.t()
|
|
]
|
|
def recent_pending_transactions(options \\ [], old_ui? \\ true)
|
|
when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
method_id_filter = Keyword.get(options, :method)
|
|
type_filter = Keyword.get(options, :type)
|
|
|
|
Transaction
|
|
|> page_pending_transaction(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> pending_transactions_query()
|
|
|> apply_filter_by_method_id_to_transactions(method_id_filter)
|
|
|> apply_filter_by_tx_type_to_transactions(type_filter)
|
|
|> order_by([transaction], desc: transaction.inserted_at, asc: transaction.hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
def pending_transactions_query(query) do
|
|
from(transaction in query,
|
|
where: is_nil(transaction.block_hash) and (is_nil(transaction.error) or transaction.error != "dropped/replaced")
|
|
)
|
|
end
|
|
|
|
def pending_transactions_list do
|
|
query =
|
|
from(transaction in Transaction,
|
|
where: is_nil(transaction.block_hash) and (is_nil(transaction.error) or transaction.error != "dropped/replaced")
|
|
)
|
|
|
|
query
|
|
|> Repo.all(timeout: :infinity)
|
|
end
|
|
|
|
@doc """
|
|
The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.Address.t/0`.
|
|
|
|
iex> Explorer.Chain.string_to_address_hash("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
|
|
{
|
|
:ok,
|
|
%Explorer.Chain.Hash{
|
|
byte_count: 20,
|
|
bytes: <<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53,
|
|
231, 239, 27, 234, 237>>
|
|
}
|
|
}
|
|
|
|
iex> Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
{
|
|
:ok,
|
|
%Explorer.Chain.Hash{
|
|
byte_count: 20,
|
|
bytes: <<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53,
|
|
231, 239, 27, 234, 237>>
|
|
}
|
|
}
|
|
|
|
iex> Base.encode16(<<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, 231, 239, 27, 234, 237>>, case: :lower)
|
|
"5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"
|
|
|
|
`String.t` format must always have 40 hexadecimal digits after the `0x` base prefix.
|
|
|
|
iex> Explorer.Chain.string_to_address_hash("0x0")
|
|
:error
|
|
|
|
"""
|
|
@spec string_to_address_hash(String.t()) :: {:ok, Hash.Address.t()} | :error
|
|
def string_to_address_hash(string) when is_binary(string) do
|
|
Hash.Address.cast(string)
|
|
end
|
|
|
|
def string_to_address_hash(_), do: :error
|
|
|
|
@doc """
|
|
The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.t/0`.
|
|
|
|
iex> Explorer.Chain.string_to_block_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
{
|
|
:ok,
|
|
%Explorer.Chain.Hash{
|
|
byte_count: 32,
|
|
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
|
|
}
|
|
}
|
|
|
|
`String.t` format must always have 64 hexadecimal digits after the `0x` base prefix.
|
|
|
|
iex> Explorer.Chain.string_to_block_hash("0x0")
|
|
:error
|
|
|
|
"""
|
|
@spec string_to_block_hash(String.t()) :: {:ok, Hash.t()} | :error
|
|
def string_to_block_hash(string) when is_binary(string) do
|
|
Hash.Full.cast(string)
|
|
end
|
|
|
|
def string_to_block_hash(_), do: :error
|
|
|
|
@doc """
|
|
The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.t/0`.
|
|
|
|
iex> Explorer.Chain.string_to_transaction_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
{
|
|
:ok,
|
|
%Explorer.Chain.Hash{
|
|
byte_count: 32,
|
|
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
|
|
}
|
|
}
|
|
|
|
`String.t` format must always have 64 hexadecimal digits after the `0x` base prefix.
|
|
|
|
iex> Explorer.Chain.string_to_transaction_hash("0x0")
|
|
:error
|
|
|
|
"""
|
|
@spec string_to_transaction_hash(String.t()) :: {:ok, Hash.t()} | :error
|
|
def string_to_transaction_hash(string) when is_binary(string) do
|
|
Hash.Full.cast(string)
|
|
end
|
|
|
|
def string_to_transaction_hash(_), do: :error
|
|
|
|
@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.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{index}`). Results will be the internal transactions older than
|
|
the `index` that is passed.
|
|
|
|
"""
|
|
|
|
@spec all_transaction_to_internal_transactions(Hash.Full.t(), [
|
|
paging_options | necessity_by_association_option | api?
|
|
]) :: [
|
|
InternalTransaction.t()
|
|
]
|
|
def all_transaction_to_internal_transactions(hash, options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
InternalTransaction
|
|
|> for_parent_transaction(hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> InternalTransaction.where_nonpending_block()
|
|
|> page_internal_transaction(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by([internal_transaction], asc: internal_transaction.index)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec transaction_to_internal_transactions(Hash.Full.t(), [paging_options | necessity_by_association_option | api?]) ::
|
|
[
|
|
InternalTransaction.t()
|
|
]
|
|
def transaction_to_internal_transactions(hash, options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
InternalTransaction
|
|
|> for_parent_transaction(hash)
|
|
|> join_associations(necessity_by_association)
|
|
|> where_transaction_has_multiple_internal_transactions()
|
|
|> InternalTransaction.where_is_different_from_parent_transaction()
|
|
|> InternalTransaction.where_nonpending_block()
|
|
|> page_internal_transaction(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by([internal_transaction], asc: internal_transaction.index)
|
|
|> preload(:block)
|
|
|> select_repo(options).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`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (a tuple of the lowest/oldest `{index}`). Results will be the transactions older than
|
|
the `index` that are passed.
|
|
|
|
"""
|
|
@spec transaction_to_logs(Hash.Full.t(), [paging_options | necessity_by_association_option | api?]) :: [Log.t()]
|
|
def transaction_to_logs(transaction_hash, options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
log_with_transactions =
|
|
from(log in Log,
|
|
inner_join: transaction in Transaction,
|
|
on:
|
|
transaction.block_hash == log.block_hash and transaction.block_number == log.block_number and
|
|
transaction.hash == log.transaction_hash
|
|
)
|
|
|
|
query =
|
|
log_with_transactions
|
|
|> where([_, transaction], transaction.hash == ^transaction_hash)
|
|
|> page_transaction_logs(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by([log], asc: log.index)
|
|
|> join_associations(necessity_by_association)
|
|
|
|
query
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Finds all `t:Explorer.Chain.TokenTransfer.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.TokenTransfer.t/0` has no associated record for that association, then the
|
|
`t:Explorer.Chain.TokenTransfer.t/0` will not be included in the page `entries`.
|
|
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
|
|
`:key` (in the form of `%{"inserted_at" => inserted_at}`). Results will be the transactions older than
|
|
the `index` that are passed.
|
|
|
|
"""
|
|
@spec transaction_to_token_transfers(Hash.Full.t(), [paging_options | necessity_by_association_option | api?()]) :: [
|
|
TokenTransfer.t()
|
|
]
|
|
def transaction_to_token_transfers(transaction_hash, options \\ []) when is_list(options) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
paging_options = options |> Keyword.get(:paging_options, @default_paging_options) |> Map.put(:asc_order, true)
|
|
token_type = Keyword.get(options, :token_type)
|
|
|
|
TokenTransfer
|
|
|> join(:inner, [token_transfer], transaction in assoc(token_transfer, :transaction))
|
|
|> where(
|
|
[token_transfer, transaction],
|
|
transaction.hash == ^transaction_hash and token_transfer.block_hash == transaction.block_hash and
|
|
token_transfer.block_number == transaction.block_number
|
|
)
|
|
|> join(:inner, [tt], token in assoc(tt, :token), as: :token)
|
|
|> preload([token: token], [{:token, token}])
|
|
|> TokenTransfer.filter_by_type(token_type)
|
|
|> TokenTransfer.page_token_transfer(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> order_by([token_transfer], asc: token_transfer.log_index)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@doc """
|
|
Converts `transaction` to the status of the `t:Explorer.Chain.Transaction.t/0` whether pending or collated.
|
|
|
|
## Returns
|
|
|
|
* `:pending` - the transaction has not be confirmed in a block yet.
|
|
* `:awaiting_internal_transactions` - the transaction happened in a pre-Byzantium block or on a chain like Ethereum
|
|
Classic (ETC) that never adopted [EIP-658](https://github.com/Arachnid/EIPs/blob/master/EIPS/eip-658.md), which
|
|
add transaction status to transaction receipts, so the status can only be derived whether the first internal
|
|
transaction has an error.
|
|
* `:success` - the transaction has been confirmed in a block
|
|
* `{:error, :awaiting_internal_transactions}` - the transactions happened post-Byzantium, but the error message
|
|
requires the internal transactions.
|
|
* `{:error, reason}` - the transaction failed due to `reason` in its first internal transaction.
|
|
|
|
"""
|
|
@spec transaction_to_status(Transaction.t()) ::
|
|
:pending
|
|
| :awaiting_internal_transactions
|
|
| :success
|
|
| {:error, :awaiting_internal_transactions}
|
|
| {:error, reason :: String.t()}
|
|
def transaction_to_status(%Transaction{error: "dropped/replaced"}), do: {:error, "dropped/replaced"}
|
|
def transaction_to_status(%Transaction{block_hash: nil, status: nil}), do: :pending
|
|
def transaction_to_status(%Transaction{status: nil}), do: :awaiting_internal_transactions
|
|
def transaction_to_status(%Transaction{status: :ok}), do: :success
|
|
|
|
def transaction_to_status(%Transaction{status: :error, error: nil}),
|
|
do: {:error, :awaiting_internal_transactions}
|
|
|
|
def transaction_to_status(%Transaction{status: :error, error: error}) when is_binary(error), do: {:error, error}
|
|
|
|
def transaction_to_revert_reason(transaction) do
|
|
%Transaction{revert_reason: revert_reason} = transaction
|
|
|
|
if revert_reason == nil do
|
|
fetch_tx_revert_reason(transaction)
|
|
else
|
|
revert_reason
|
|
end
|
|
end
|
|
|
|
def fetch_tx_revert_reason(
|
|
%Transaction{
|
|
block_number: block_number,
|
|
to_address_hash: to_address_hash,
|
|
from_address_hash: from_address_hash,
|
|
input: data,
|
|
gas: gas,
|
|
gas_price: gas_price,
|
|
value: value
|
|
} = transaction
|
|
) do
|
|
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
|
|
|
|
gas_hex =
|
|
if gas do
|
|
gas_hex_without_prefix =
|
|
gas
|
|
|> Decimal.to_integer()
|
|
|> Integer.to_string(16)
|
|
|> String.downcase()
|
|
|
|
"0x" <> gas_hex_without_prefix
|
|
else
|
|
"0x0"
|
|
end
|
|
|
|
req =
|
|
EthereumJSONRPCTransaction.eth_call_request(
|
|
0,
|
|
block_number,
|
|
data,
|
|
to_address_hash,
|
|
from_address_hash,
|
|
gas_hex,
|
|
Wei.hex_format(gas_price),
|
|
Wei.hex_format(value)
|
|
)
|
|
|
|
revert_reason =
|
|
case EthereumJSONRPC.json_rpc(req, json_rpc_named_arguments) do
|
|
{:error, %{data: data}} ->
|
|
data
|
|
|
|
{:error, %{message: message}} ->
|
|
message
|
|
|
|
_ ->
|
|
""
|
|
end
|
|
|
|
formatted_revert_reason =
|
|
revert_reason |> format_revert_reason_message() |> (&if(String.valid?(&1), do: &1, else: revert_reason)).()
|
|
|
|
if byte_size(formatted_revert_reason) > 0 do
|
|
transaction
|
|
|> Changeset.change(%{revert_reason: formatted_revert_reason})
|
|
|> Repo.update()
|
|
end
|
|
|
|
formatted_revert_reason
|
|
end
|
|
|
|
def format_revert_reason_message(revert_reason) do
|
|
case revert_reason do
|
|
@revert_msg_prefix_1 <> rest ->
|
|
rest
|
|
|
|
@revert_msg_prefix_2 <> rest ->
|
|
rest
|
|
|
|
@revert_msg_prefix_3 <> rest ->
|
|
extract_revert_reason_message_wrapper(rest)
|
|
|
|
@revert_msg_prefix_4 <> rest ->
|
|
extract_revert_reason_message_wrapper(rest)
|
|
|
|
@revert_msg_prefix_5 <> rest ->
|
|
extract_revert_reason_message_wrapper(rest)
|
|
|
|
revert_reason_full ->
|
|
revert_reason_full
|
|
end
|
|
end
|
|
|
|
defp extract_revert_reason_message_wrapper(revert_reason_message) do
|
|
case revert_reason_message do
|
|
"0x" <> hex ->
|
|
extract_revert_reason_message(hex)
|
|
|
|
_ ->
|
|
revert_reason_message
|
|
end
|
|
end
|
|
|
|
defp extract_revert_reason_message(hex) do
|
|
case hex do
|
|
@revert_error_method_id <> msg_with_offset ->
|
|
[msg] =
|
|
msg_with_offset
|
|
|> Base.decode16!(case: :mixed)
|
|
|> TypeDecoder.decode_raw([:string])
|
|
|
|
msg
|
|
|
|
_ ->
|
|
hex
|
|
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.wei()
|
|
@spec value(InternalTransaction.t(), :gwei) :: Wei.gwei()
|
|
@spec value(InternalTransaction.t(), :ether) :: Wei.ether()
|
|
@spec value(Transaction.t(), :wei) :: Wei.wei()
|
|
@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
|
|
|
|
def smart_contract_bytecode(address_hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^address_hash,
|
|
select: address.contract_code
|
|
)
|
|
|
|
query
|
|
|> Repo.one()
|
|
|> Data.to_string()
|
|
end
|
|
|
|
def smart_contract_creation_tx_bytecode(address_hash) do
|
|
creation_tx_query =
|
|
from(
|
|
tx in Transaction,
|
|
left_join: a in Address,
|
|
on: tx.created_contract_address_hash == a.hash,
|
|
where: tx.created_contract_address_hash == ^address_hash,
|
|
where: tx.status == ^1,
|
|
select: %{init: tx.input, created_contract_code: a.contract_code}
|
|
)
|
|
|
|
tx_input =
|
|
creation_tx_query
|
|
|> Repo.one()
|
|
|
|
if tx_input do
|
|
with %{init: input, created_contract_code: created_contract_code} <- tx_input do
|
|
%{init: Data.to_string(input), created_contract_code: Data.to_string(created_contract_code)}
|
|
end
|
|
else
|
|
creation_int_tx_query =
|
|
from(
|
|
itx in InternalTransaction,
|
|
join: t in assoc(itx, :transaction),
|
|
where: itx.created_contract_address_hash == ^address_hash,
|
|
where: t.status == ^1,
|
|
select: %{init: itx.init, created_contract_code: itx.created_contract_code}
|
|
)
|
|
|
|
res = creation_int_tx_query |> Repo.one()
|
|
|
|
case res do
|
|
%{init: init, created_contract_code: created_contract_code} ->
|
|
init_str = Data.to_string(init)
|
|
created_contract_code_str = Data.to_string(created_contract_code)
|
|
%{init: init_str, created_contract_code: created_contract_code_str}
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if an address is a contract
|
|
"""
|
|
@spec contract_address?(String.t(), non_neg_integer(), Keyword.t()) :: boolean() | :json_rpc_error
|
|
def contract_address?(address_hash, block_number, json_rpc_named_arguments \\ []) do
|
|
{:ok, binary_hash} = Explorer.Chain.Hash.Address.cast(address_hash)
|
|
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^binary_hash
|
|
)
|
|
|
|
address = Repo.one(query)
|
|
|
|
cond do
|
|
is_nil(address) ->
|
|
block_quantity = integer_to_quantity(block_number)
|
|
|
|
case EthereumJSONRPC.fetch_codes(
|
|
[%{block_quantity: block_quantity, address: address_hash}],
|
|
json_rpc_named_arguments
|
|
) do
|
|
{:ok, %EthereumJSONRPC.FetchedCodes{params_list: fetched_codes}} ->
|
|
result = List.first(fetched_codes)
|
|
|
|
result && !(is_nil(result[:code]) || result[:code] == "" || result[:code] == "0x")
|
|
|
|
_ ->
|
|
:json_rpc_error
|
|
end
|
|
|
|
is_nil(address.contract_code) ->
|
|
false
|
|
|
|
true ->
|
|
true
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Fetches contract creation input data.
|
|
"""
|
|
@spec contract_creation_input_data(String.t()) :: nil | String.t()
|
|
def contract_creation_input_data(address_hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^address_hash,
|
|
preload: [:contracts_creation_internal_transaction, :contracts_creation_transaction]
|
|
)
|
|
|
|
contract_address = Repo.one(query)
|
|
|
|
contract_creation_input_data_from_address(contract_address)
|
|
end
|
|
|
|
# credo:disable-for-next-line /Complexity/
|
|
defp contract_creation_input_data_from_address(address) do
|
|
internal_transaction = address && address.contracts_creation_internal_transaction
|
|
transaction = address && address.contracts_creation_transaction
|
|
|
|
cond do
|
|
is_nil(address) ->
|
|
""
|
|
|
|
internal_transaction && internal_transaction.input ->
|
|
Data.to_string(internal_transaction.input)
|
|
|
|
internal_transaction && internal_transaction.init ->
|
|
Data.to_string(internal_transaction.init)
|
|
|
|
transaction && transaction.input ->
|
|
Data.to_string(transaction.input)
|
|
|
|
is_nil(transaction) && is_nil(internal_transaction) &&
|
|
not is_nil(address.contract_code) ->
|
|
%Explorer.Chain.Data{bytes: bytes} = address.contract_code
|
|
Base.encode16(bytes, case: :lower)
|
|
|
|
true ->
|
|
""
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Inserts a `t:SmartContract.t/0`.
|
|
|
|
As part of inserting a new smart contract, an additional record is inserted for
|
|
naming the address for reference.
|
|
"""
|
|
@spec create_smart_contract(map()) :: {:ok, SmartContract.t()} | {:error, Ecto.Changeset.t()}
|
|
def create_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do
|
|
new_contract = %SmartContract{}
|
|
|
|
attrs =
|
|
attrs
|
|
|> Helper.add_contract_code_md5()
|
|
|
|
smart_contract_changeset =
|
|
new_contract
|
|
|> SmartContract.changeset(attrs)
|
|
|> Changeset.put_change(:external_libraries, external_libraries)
|
|
|
|
new_contract_additional_source = %SmartContractAdditionalSource{}
|
|
|
|
smart_contract_additional_sources_changesets =
|
|
if secondary_sources do
|
|
secondary_sources
|
|
|> Enum.map(fn changeset ->
|
|
new_contract_additional_source
|
|
|> SmartContractAdditionalSource.changeset(changeset)
|
|
end)
|
|
else
|
|
[]
|
|
end
|
|
|
|
address_hash = Changeset.get_field(smart_contract_changeset, :address_hash)
|
|
|
|
# Enforce ShareLocks tables order (see docs: sharelocks.md)
|
|
insert_contract_query =
|
|
Multi.new()
|
|
|> Multi.run(:set_address_verified, fn repo, _ -> set_address_verified(repo, address_hash) end)
|
|
|> Multi.run(:clear_primary_address_names, fn repo, _ -> clear_primary_address_names(repo, address_hash) end)
|
|
|> Multi.insert(:smart_contract, smart_contract_changeset)
|
|
|
|
insert_contract_query_with_additional_sources =
|
|
smart_contract_additional_sources_changesets
|
|
|> Enum.with_index()
|
|
|> Enum.reduce(insert_contract_query, fn {changeset, index}, multi ->
|
|
Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset)
|
|
end)
|
|
|
|
insert_result =
|
|
insert_contract_query_with_additional_sources
|
|
|> Repo.transaction()
|
|
|
|
create_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash)
|
|
|
|
case insert_result do
|
|
{:ok, %{smart_contract: smart_contract}} ->
|
|
{:ok, smart_contract}
|
|
|
|
{:error, :smart_contract, changeset, _} ->
|
|
{:error, changeset}
|
|
|
|
{:error, :set_address_verified, message, _} ->
|
|
{:error, message}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates a `t:SmartContract.t/0`.
|
|
|
|
Has the similar logic as create_smart_contract/1.
|
|
Used in cases when you need to update row in DB contains SmartContract, e.g. in case of changing
|
|
status `partially verified` to `fully verified` (re-verify).
|
|
"""
|
|
@spec update_smart_contract(map()) :: {:ok, SmartContract.t()} | {:error, Ecto.Changeset.t()}
|
|
def update_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do
|
|
address_hash = Map.get(attrs, :address_hash)
|
|
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^address_hash
|
|
)
|
|
|
|
query_sources =
|
|
from(
|
|
source in SmartContractAdditionalSource,
|
|
where: source.address_hash == ^address_hash
|
|
)
|
|
|
|
_delete_sources = Repo.delete_all(query_sources)
|
|
|
|
smart_contract = Repo.one(query)
|
|
|
|
smart_contract_changeset =
|
|
smart_contract
|
|
|> SmartContract.changeset(attrs)
|
|
|> Changeset.put_change(:external_libraries, external_libraries)
|
|
|
|
new_contract_additional_source = %SmartContractAdditionalSource{}
|
|
|
|
smart_contract_additional_sources_changesets =
|
|
if secondary_sources do
|
|
secondary_sources
|
|
|> Enum.map(fn changeset ->
|
|
new_contract_additional_source
|
|
|> SmartContractAdditionalSource.changeset(changeset)
|
|
end)
|
|
else
|
|
[]
|
|
end
|
|
|
|
# Enforce ShareLocks tables order (see docs: sharelocks.md)
|
|
insert_contract_query =
|
|
Multi.new()
|
|
|> Multi.update(:smart_contract, smart_contract_changeset)
|
|
|
|
insert_contract_query_with_additional_sources =
|
|
smart_contract_additional_sources_changesets
|
|
|> Enum.with_index()
|
|
|> Enum.reduce(insert_contract_query, fn {changeset, index}, multi ->
|
|
Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset)
|
|
end)
|
|
|
|
insert_result =
|
|
insert_contract_query_with_additional_sources
|
|
|> Repo.transaction()
|
|
|
|
case insert_result do
|
|
{:ok, %{smart_contract: smart_contract}} ->
|
|
{:ok, smart_contract}
|
|
|
|
{:error, :smart_contract, changeset, _} ->
|
|
{:error, changeset}
|
|
|
|
{:error, :set_address_verified, message, _} ->
|
|
{:error, message}
|
|
end
|
|
end
|
|
|
|
defp set_address_verified(repo, address_hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^address_hash
|
|
)
|
|
|
|
case repo.update_all(query, set: [verified: true]) do
|
|
{1, _} -> {:ok, []}
|
|
_ -> {:error, "There was an error annotating that the address has been verified."}
|
|
end
|
|
end
|
|
|
|
defp set_address_decompiled(repo, address_hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^address_hash
|
|
)
|
|
|
|
case repo.update_all(query, set: [decompiled: true]) do
|
|
{1, _} -> {:ok, []}
|
|
_ -> {:error, "There was an error annotating that the address has been decompiled."}
|
|
end
|
|
end
|
|
|
|
defp clear_primary_address_names(repo, address_hash) do
|
|
query =
|
|
from(
|
|
address_name in Address.Name,
|
|
where: address_name.address_hash == ^address_hash,
|
|
# Enforce Name ShareLocks order (see docs: sharelocks.md)
|
|
order_by: [asc: :address_hash, asc: :name],
|
|
lock: "FOR NO KEY UPDATE"
|
|
)
|
|
|
|
repo.update_all(
|
|
from(n in Address.Name, join: s in subquery(query), on: n.address_hash == s.address_hash and n.name == s.name),
|
|
set: [primary: false]
|
|
)
|
|
|
|
{:ok, []}
|
|
end
|
|
|
|
defp create_address_name(repo, name, address_hash) do
|
|
params = %{
|
|
address_hash: address_hash,
|
|
name: name,
|
|
primary: true
|
|
}
|
|
|
|
%Address.Name{}
|
|
|> Address.Name.changeset(params)
|
|
|> repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name])
|
|
end
|
|
|
|
def get_verified_twin_contract(%Explorer.Chain.Address{} = target_address, options \\ []) do
|
|
case target_address do
|
|
%{contract_code: %Chain.Data{bytes: contract_code_bytes}} ->
|
|
target_address_hash = target_address.hash
|
|
|
|
contract_code_md5 = Helper.contract_code_md5(contract_code_bytes)
|
|
|
|
verified_contract_twin_query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.contract_code_md5 == ^contract_code_md5,
|
|
where: smart_contract.address_hash != ^target_address_hash,
|
|
select: smart_contract,
|
|
limit: 1
|
|
)
|
|
|
|
verified_contract_twin_query
|
|
|> select_repo(options).one(timeout: 10_000)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Finds metadata for verification of a contract from verified twins: contracts with the same bytecode
|
|
which were verified previously, returns a single t:SmartContract.t/0
|
|
"""
|
|
def get_address_verified_twin_contract(hash, options \\ [])
|
|
|
|
def get_address_verified_twin_contract(hash, options) when is_binary(hash) do
|
|
case string_to_address_hash(hash) do
|
|
{:ok, address_hash} -> get_address_verified_twin_contract(address_hash, options)
|
|
_ -> %{:verified_contract => nil, :additional_sources => nil}
|
|
end
|
|
end
|
|
|
|
def get_address_verified_twin_contract(%Explorer.Chain.Hash{} = address_hash, options) do
|
|
with target_address <- select_repo(options).get(Address, address_hash),
|
|
false <- is_nil(target_address) do
|
|
verified_contract_twin = get_verified_twin_contract(target_address, options)
|
|
|
|
verified_contract_twin_additional_sources = get_contract_additional_sources(verified_contract_twin, options)
|
|
|
|
%{
|
|
:verified_contract => check_and_update_constructor_args(verified_contract_twin),
|
|
:additional_sources => verified_contract_twin_additional_sources
|
|
}
|
|
else
|
|
_ ->
|
|
%{:verified_contract => nil, :additional_sources => nil}
|
|
end
|
|
end
|
|
|
|
def get_minimal_proxy_template(address_hash, options \\ []) do
|
|
minimal_proxy_template =
|
|
case select_repo(options).get(Address, address_hash) do
|
|
nil ->
|
|
nil
|
|
|
|
target_address ->
|
|
contract_code = target_address.contract_code
|
|
|
|
case contract_code do
|
|
%Chain.Data{bytes: contract_code_bytes} ->
|
|
contract_bytecode = Base.encode16(contract_code_bytes, case: :lower)
|
|
|
|
get_minimal_proxy_from_template_code(contract_bytecode, options)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
minimal_proxy_template
|
|
end
|
|
|
|
defp get_minimal_proxy_from_template_code(contract_bytecode, options) do
|
|
case contract_bytecode do
|
|
"363d3d373d3d3d363d73" <> <<template_address::binary-size(40)>> <> _ ->
|
|
template_address = "0x" <> template_address
|
|
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^template_address,
|
|
select: smart_contract
|
|
)
|
|
|
|
template =
|
|
query
|
|
|> select_repo(options).one(timeout: 10_000)
|
|
|
|
template
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp get_contract_additional_sources(verified_contract_twin, options) do
|
|
if verified_contract_twin do
|
|
verified_contract_twin_additional_sources_query =
|
|
from(
|
|
s in SmartContractAdditionalSource,
|
|
where: s.address_hash == ^verified_contract_twin.address_hash
|
|
)
|
|
|
|
verified_contract_twin_additional_sources_query
|
|
|> select_repo(options).all()
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
@spec address_hash_to_smart_contract(Hash.Address.t(), [api?]) :: SmartContract.t() | nil
|
|
def address_hash_to_smart_contract(address_hash, options \\ []) do
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^address_hash
|
|
)
|
|
|
|
current_smart_contract = select_repo(options).one(query)
|
|
|
|
if current_smart_contract do
|
|
current_smart_contract
|
|
else
|
|
address_verified_twin_contract =
|
|
get_minimal_proxy_template(address_hash, options) ||
|
|
get_address_verified_twin_contract(address_hash, options).verified_contract
|
|
|
|
if address_verified_twin_contract do
|
|
address_verified_twin_contract
|
|
|> Map.put(:address_hash, address_hash)
|
|
|> Map.put(:metadata_from_verified_twin, true)
|
|
|> Map.put(:implementation_address_hash, nil)
|
|
|> Map.put(:implementation_name, nil)
|
|
|> Map.put(:implementation_fetched_at, nil)
|
|
else
|
|
current_smart_contract
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec address_hash_to_smart_contract(Hash.Address.t()) :: SmartContract.t() | nil
|
|
def address_hash_to_one_smart_contract(hash) do
|
|
SmartContract
|
|
|> where([sc], sc.address_hash == ^hash)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@spec address_hash_to_smart_contract_without_twin(Hash.Address.t(), [api?]) :: SmartContract.t() | nil
|
|
def address_hash_to_smart_contract_without_twin(address_hash, options) do
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^address_hash
|
|
)
|
|
|
|
select_repo(options).one(query)
|
|
end
|
|
|
|
def smart_contract_fully_verified?(address_hash, options \\ [])
|
|
|
|
def smart_contract_fully_verified?(address_hash_str, options) when is_binary(address_hash_str) do
|
|
case string_to_address_hash(address_hash_str) do
|
|
{:ok, address_hash} ->
|
|
check_fully_verified(address_hash, options)
|
|
|
|
_ ->
|
|
false
|
|
end
|
|
end
|
|
|
|
def smart_contract_fully_verified?(address_hash, options) do
|
|
check_fully_verified(address_hash, options)
|
|
end
|
|
|
|
defp check_fully_verified(address_hash, options) do
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^address_hash
|
|
)
|
|
|
|
result = select_repo(options).one(query)
|
|
|
|
if result, do: !result.partially_verified, else: false
|
|
end
|
|
|
|
def smart_contract_verified?(address_hash_str) when is_binary(address_hash_str) do
|
|
case string_to_address_hash(address_hash_str) do
|
|
{:ok, address_hash} ->
|
|
check_verified(address_hash)
|
|
|
|
_ ->
|
|
false
|
|
end
|
|
end
|
|
|
|
def smart_contract_verified?(address_hash) do
|
|
check_verified(address_hash)
|
|
end
|
|
|
|
defp check_verified(address_hash) do
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^address_hash
|
|
)
|
|
|
|
if Repo.one(query), do: true, else: false
|
|
end
|
|
|
|
defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil, with_pending? \\ false) do
|
|
Transaction
|
|
|> order_for_transactions(with_pending?)
|
|
|> where_block_number_in_period(from_block, to_block)
|
|
|> handle_paging_options(paging_options)
|
|
end
|
|
|
|
defp order_for_transactions(query, true) do
|
|
query
|
|
|> order_by([transaction],
|
|
desc: transaction.block_number,
|
|
desc: transaction.index,
|
|
desc: transaction.inserted_at,
|
|
asc: transaction.hash
|
|
)
|
|
end
|
|
|
|
defp order_for_transactions(query, _) do
|
|
query
|
|
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
|
|
end
|
|
|
|
defp fetch_transactions_in_ascending_order_by_index(paging_options) do
|
|
Transaction
|
|
|> order_by([transaction], asc: transaction.index)
|
|
|> handle_block_paging_options(paging_options)
|
|
end
|
|
|
|
defp for_parent_transaction(query, %Hash{byte_count: unquote(Hash.Full.byte_count())} = hash) do
|
|
from(
|
|
child in query,
|
|
inner_join: transaction in assoc(child, :transaction),
|
|
where: transaction.hash == ^hash
|
|
)
|
|
end
|
|
|
|
defp handle_block_paging_options(query, nil), do: query
|
|
|
|
defp handle_block_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query
|
|
|
|
defp handle_block_paging_options(query, paging_options) do
|
|
query
|
|
|> page_block_transactions(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
end
|
|
|
|
defp handle_paging_options(query, nil), do: query
|
|
|
|
defp handle_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query
|
|
|
|
defp handle_paging_options(query, paging_options) do
|
|
query
|
|
|> page_transaction(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
end
|
|
|
|
defp handle_verified_contracts_paging_options(query, nil), do: query
|
|
|
|
defp handle_verified_contracts_paging_options(query, paging_options) do
|
|
query
|
|
|> page_verified_contracts(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
end
|
|
|
|
defp handle_withdrawals_paging_options(query, nil), do: query
|
|
|
|
defp handle_withdrawals_paging_options(query, paging_options) do
|
|
query
|
|
|> Withdrawal.page_withdrawals(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
end
|
|
|
|
defp handle_random_access_paging_options(query, empty_options) when empty_options in [nil, [], %{}],
|
|
do: limit(query, ^(@default_page_size + 1))
|
|
|
|
defp handle_random_access_paging_options(query, paging_options) do
|
|
query
|
|
|> (&if(paging_options |> Map.get(:page_number, 1) |> process_page_number() == 1,
|
|
do: &1,
|
|
else: page_transaction(&1, paging_options)
|
|
)).()
|
|
|> handle_page(paging_options)
|
|
end
|
|
|
|
defp handle_page(query, paging_options) do
|
|
page_number = paging_options |> Map.get(:page_number, 1) |> process_page_number()
|
|
page_size = Map.get(paging_options, :page_size, @default_page_size)
|
|
|
|
cond do
|
|
page_in_bounds?(page_number, page_size) && page_number == 1 ->
|
|
query
|
|
|> limit(^(page_size + 1))
|
|
|
|
page_in_bounds?(page_number, page_size) ->
|
|
query
|
|
|> limit(^page_size)
|
|
|> offset(^((page_number - 2) * page_size))
|
|
|
|
true ->
|
|
query
|
|
|> limit(^(@default_page_size + 1))
|
|
end
|
|
end
|
|
|
|
defp process_page_number(number) when number < 1, do: 1
|
|
|
|
defp process_page_number(number), do: number
|
|
|
|
defp page_in_bounds?(page_number, page_size),
|
|
do: page_size <= @limit_showing_transactions && @limit_showing_transactions - page_number * page_size >= 0
|
|
|
|
def limit_showing_transactions, do: @limit_showing_transactions
|
|
|
|
defp join_association(query, [{association, nested_preload}], necessity)
|
|
when is_atom(association) and is_atom(nested_preload) do
|
|
case necessity do
|
|
:optional ->
|
|
preload(query, [{^association, ^nested_preload}])
|
|
|
|
:required ->
|
|
from(q in query,
|
|
inner_join: a in assoc(q, ^association),
|
|
left_join: b in assoc(a, ^nested_preload),
|
|
preload: [{^association, {a, [{^nested_preload, b}]}}]
|
|
)
|
|
end
|
|
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_association(query, association, necessity) 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 page_addresses(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_addresses(query, %PagingOptions{key: {coin_balance, hash}}) do
|
|
from(address in query,
|
|
where:
|
|
(address.fetched_coin_balance == ^coin_balance and address.hash > ^hash) or
|
|
address.fetched_coin_balance < ^coin_balance
|
|
)
|
|
end
|
|
|
|
defp page_blocks(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_blocks(query, %PagingOptions{key: {block_number}}) do
|
|
where(query, [block], block.number < ^block_number)
|
|
end
|
|
|
|
defp page_coin_balances(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_coin_balances(query, %PagingOptions{key: {block_number}}) do
|
|
where(query, [coin_balance], coin_balance.block_number < ^block_number)
|
|
end
|
|
|
|
defp page_internal_transaction(_, _, _ \\ %{index_int_tx_desc_order: false})
|
|
|
|
defp page_internal_transaction(query, %PagingOptions{key: nil}, _), do: query
|
|
|
|
defp page_internal_transaction(query, %PagingOptions{key: {block_number, transaction_index, index}}, %{
|
|
index_int_tx_desc_order: desc
|
|
}) do
|
|
hardcoded_where_for_page_int_tx(query, block_number, transaction_index, index, desc)
|
|
end
|
|
|
|
defp page_internal_transaction(query, %PagingOptions{key: {index}}, %{index_int_tx_desc_order: desc}) do
|
|
if desc do
|
|
where(query, [internal_transaction], internal_transaction.index < ^index)
|
|
else
|
|
where(query, [internal_transaction], internal_transaction.index > ^index)
|
|
end
|
|
end
|
|
|
|
defp hardcoded_where_for_page_int_tx(query, block_number, transaction_index, index, false),
|
|
do:
|
|
where(
|
|
query,
|
|
[internal_transaction],
|
|
internal_transaction.block_number < ^block_number or
|
|
(internal_transaction.block_number == ^block_number and
|
|
internal_transaction.transaction_index < ^transaction_index) or
|
|
(internal_transaction.block_number == ^block_number and
|
|
internal_transaction.transaction_index == ^transaction_index and internal_transaction.index > ^index)
|
|
)
|
|
|
|
defp hardcoded_where_for_page_int_tx(query, block_number, transaction_index, index, true),
|
|
do:
|
|
where(
|
|
query,
|
|
[internal_transaction],
|
|
internal_transaction.block_number < ^block_number or
|
|
(internal_transaction.block_number == ^block_number and
|
|
internal_transaction.transaction_index < ^transaction_index) or
|
|
(internal_transaction.block_number == ^block_number and
|
|
internal_transaction.transaction_index == ^transaction_index and internal_transaction.index < ^index)
|
|
)
|
|
|
|
defp page_logs(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_logs(query, %PagingOptions{key: {index}}) do
|
|
where(query, [log], log.index > ^index)
|
|
end
|
|
|
|
defp page_logs(query, %PagingOptions{key: {block_number, log_index}}) do
|
|
where(
|
|
query,
|
|
[log],
|
|
log.block_number < ^block_number or (log.block_number == ^block_number and log.index < ^log_index)
|
|
)
|
|
end
|
|
|
|
defp page_transaction_logs(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_transaction_logs(query, %PagingOptions{key: {index}}) do
|
|
where(query, [log], log.index > ^index)
|
|
end
|
|
|
|
defp page_transaction_logs(query, %PagingOptions{key: {_block_number, index}}) do
|
|
where(query, [log], log.index > ^index)
|
|
end
|
|
|
|
defp page_pending_transaction(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_pending_transaction(query, %PagingOptions{key: {inserted_at, hash}}) do
|
|
where(
|
|
query,
|
|
[transaction],
|
|
(is_nil(transaction.block_number) and
|
|
(transaction.inserted_at < ^inserted_at or
|
|
(transaction.inserted_at == ^inserted_at and transaction.hash > ^hash))) or
|
|
not is_nil(transaction.block_number)
|
|
)
|
|
end
|
|
|
|
defp page_transaction(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_transaction(query, %PagingOptions{is_pending_tx: true} = options),
|
|
do: page_pending_transaction(query, options)
|
|
|
|
defp page_transaction(query, %PagingOptions{key: {block_number, index}, is_index_in_asc_order: true}) do
|
|
where(
|
|
query,
|
|
[transaction],
|
|
transaction.block_number < ^block_number or
|
|
(transaction.block_number == ^block_number and transaction.index > ^index)
|
|
)
|
|
end
|
|
|
|
defp page_transaction(query, %PagingOptions{key: {block_number, index}}) do
|
|
where(
|
|
query,
|
|
[transaction],
|
|
transaction.block_number < ^block_number or
|
|
(transaction.block_number == ^block_number and transaction.index < ^index)
|
|
)
|
|
end
|
|
|
|
defp page_transaction(query, %PagingOptions{key: {index}}) do
|
|
where(query, [transaction], transaction.index < ^index)
|
|
end
|
|
|
|
defp page_block_transactions(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_block_transactions(query, %PagingOptions{key: {_block_number, index}, is_index_in_asc_order: true}) do
|
|
where(query, [transaction], transaction.index > ^index)
|
|
end
|
|
|
|
defp page_block_transactions(query, %PagingOptions{key: {_block_number, index}}) do
|
|
where(query, [transaction], transaction.index < ^index)
|
|
end
|
|
|
|
def page_token_balances(query, %PagingOptions{key: nil}), do: query
|
|
|
|
def page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do
|
|
where(
|
|
query,
|
|
[tb],
|
|
tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash)
|
|
)
|
|
end
|
|
|
|
def page_current_token_balances(query, keyword) when is_list(keyword),
|
|
do: page_current_token_balances(query, Keyword.get(keyword, :paging_options))
|
|
|
|
def page_current_token_balances(query, %PagingOptions{key: nil}), do: query
|
|
|
|
def page_current_token_balances(query, %PagingOptions{key: {nil, value, id}}) do
|
|
fiat_balance = CurrentTokenBalance.fiat_value_query()
|
|
|
|
condition =
|
|
dynamic(
|
|
[ctb, t],
|
|
is_nil(^fiat_balance) and
|
|
(ctb.value < ^value or
|
|
(ctb.value == ^value and ctb.id < ^id))
|
|
)
|
|
|
|
where(
|
|
query,
|
|
[ctb, t],
|
|
^condition
|
|
)
|
|
end
|
|
|
|
def page_current_token_balances(query, %PagingOptions{key: {fiat_value, value, id}}) do
|
|
fiat_balance = CurrentTokenBalance.fiat_value_query()
|
|
|
|
condition =
|
|
dynamic(
|
|
[ctb, t],
|
|
^fiat_balance < ^fiat_value or is_nil(^fiat_balance) or
|
|
(^fiat_balance == ^fiat_value and
|
|
(ctb.value < ^value or
|
|
(ctb.value == ^value and ctb.id < ^id)))
|
|
)
|
|
|
|
where(
|
|
query,
|
|
[ctb, t],
|
|
^condition
|
|
)
|
|
end
|
|
|
|
defp page_verified_contracts(query, %PagingOptions{key: nil}), do: query
|
|
|
|
defp page_verified_contracts(query, %PagingOptions{key: {id}}) do
|
|
where(query, [contract], contract.id < ^id)
|
|
end
|
|
|
|
@doc """
|
|
Ensures the following conditions are true:
|
|
|
|
* excludes internal transactions of type call with no siblings in the
|
|
transaction
|
|
* includes internal transactions of type create, reward, or selfdestruct
|
|
even when they are alone in the parent transaction
|
|
|
|
"""
|
|
@spec where_transaction_has_multiple_internal_transactions(Ecto.Query.t()) :: Ecto.Query.t()
|
|
def where_transaction_has_multiple_internal_transactions(query) do
|
|
where(
|
|
query,
|
|
[internal_transaction, transaction],
|
|
internal_transaction.type != ^:call or
|
|
fragment(
|
|
"""
|
|
EXISTS (SELECT sibling.*
|
|
FROM internal_transactions AS sibling
|
|
WHERE sibling.transaction_hash = ? AND sibling.index != ?
|
|
)
|
|
""",
|
|
transaction.hash,
|
|
internal_transaction.index
|
|
)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
The current total number of coins minted minus verifiably burned coins.
|
|
"""
|
|
@spec total_supply :: non_neg_integer() | nil
|
|
def total_supply do
|
|
supply_module().total() || 0
|
|
end
|
|
|
|
@doc """
|
|
The current number coins in the market for trading.
|
|
"""
|
|
@spec circulating_supply :: non_neg_integer() | nil
|
|
def circulating_supply do
|
|
supply_module().circulating()
|
|
end
|
|
|
|
defp supply_module do
|
|
Application.get_env(:explorer, :supply, Explorer.Chain.Supply.ExchangeRate)
|
|
end
|
|
|
|
@doc """
|
|
Calls supply_for_days from the configured supply_module
|
|
"""
|
|
def supply_for_days, do: supply_module().supply_for_days(MarketHistoryCache.recent_days_count())
|
|
|
|
@doc """
|
|
Streams a lists token contract addresses that haven't been cataloged.
|
|
"""
|
|
@spec stream_uncataloged_token_contract_address_hashes(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: Hash.Address.t(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_uncataloged_token_contract_address_hashes(initial, reducer, limited? \\ false)
|
|
when is_function(reducer, 2) do
|
|
query =
|
|
from(
|
|
token in Token,
|
|
where: token.cataloged == false,
|
|
select: token.contract_address_hash
|
|
)
|
|
|
|
query
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@spec stream_unfetched_token_instances(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: map(), accumulator -> accumulator)
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_unfetched_token_instances(initial, reducer) when is_function(reducer, 2) do
|
|
nft_tokens =
|
|
from(
|
|
token in Token,
|
|
where: token.type == ^"ERC-721" or token.type == ^"ERC-1155",
|
|
select: token.contract_address_hash
|
|
)
|
|
|
|
token_ids_query =
|
|
from(
|
|
token_transfer in TokenTransfer,
|
|
select: %{
|
|
token_contract_address_hash: token_transfer.token_contract_address_hash,
|
|
token_id: fragment("unnest(?)", token_transfer.token_ids)
|
|
}
|
|
)
|
|
|
|
query =
|
|
from(
|
|
transfer in subquery(token_ids_query),
|
|
inner_join: token in subquery(nft_tokens),
|
|
on: token.contract_address_hash == transfer.token_contract_address_hash,
|
|
left_join: instance in Instance,
|
|
on:
|
|
transfer.token_contract_address_hash == instance.token_contract_address_hash and
|
|
transfer.token_id == instance.token_id,
|
|
where: is_nil(instance.token_id),
|
|
select: %{
|
|
contract_address_hash: transfer.token_contract_address_hash,
|
|
token_id: transfer.token_id
|
|
}
|
|
)
|
|
|
|
distinct_query =
|
|
from(
|
|
q in subquery(query),
|
|
distinct: [q.contract_address_hash, q.token_id]
|
|
)
|
|
|
|
Repo.stream_reduce(distinct_query, initial, reducer)
|
|
end
|
|
|
|
@spec stream_token_instances_with_error(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: map(), accumulator -> accumulator),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_token_instances_with_error(initial, reducer, limited? \\ false) when is_function(reducer, 2) do
|
|
# likely to get valid metadata
|
|
high_priority = ["request error: 429", ":checkout_timeout", ":econnrefused", ":timeout"]
|
|
# almost impossible to get valid metadata
|
|
negative_priority = ["VM execution error", "no uri", "invalid json"]
|
|
|
|
Instance
|
|
|> where([instance], not is_nil(instance.error))
|
|
|> select([instance], %{
|
|
contract_address_hash: instance.token_contract_address_hash,
|
|
token_id: instance.token_id,
|
|
updated_at: instance.updated_at
|
|
})
|
|
|> order_by([instance], desc: instance.error in ^high_priority, asc: instance.error in ^negative_priority)
|
|
|> add_fetcher_limit(limited?)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
Streams a list of token contract addresses that have been cataloged.
|
|
"""
|
|
@spec stream_cataloged_token_contract_address_hashes(
|
|
initial :: accumulator,
|
|
reducer :: (entry :: Hash.Address.t(), accumulator -> accumulator),
|
|
some_time_ago_updated :: integer(),
|
|
limited? :: boolean()
|
|
) :: {:ok, accumulator}
|
|
when accumulator: term()
|
|
def stream_cataloged_token_contract_address_hashes(initial, reducer, some_time_ago_updated \\ 2880, limited? \\ false)
|
|
when is_function(reducer, 2) do
|
|
some_time_ago_updated
|
|
|> Token.cataloged_tokens()
|
|
|> add_fetcher_limit(limited?)
|
|
|> order_by(asc: :updated_at)
|
|
|> Repo.stream_reduce(initial, reducer)
|
|
end
|
|
|
|
@doc """
|
|
Returns a list of block numbers token transfer `t:Log.t/0`s that don't have an
|
|
associated `t:TokenTransfer.t/0` record.
|
|
"""
|
|
def uncataloged_token_transfer_block_numbers do
|
|
query =
|
|
from(l in Log,
|
|
as: :log,
|
|
where:
|
|
l.first_topic == unquote(TokenTransfer.constant()) or
|
|
l.first_topic == unquote(TokenTransfer.erc1155_single_transfer_signature()) or
|
|
l.first_topic == unquote(TokenTransfer.erc1155_batch_transfer_signature()),
|
|
where:
|
|
not exists(
|
|
from(tf in TokenTransfer,
|
|
where: tf.transaction_hash == parent_as(:log).transaction_hash,
|
|
where: tf.log_index == parent_as(:log).index
|
|
)
|
|
),
|
|
select: l.block_number,
|
|
distinct: l.block_number
|
|
)
|
|
|
|
Repo.stream_reduce(query, [], &[&1 | &2])
|
|
end
|
|
|
|
def decode_contract_address_hash_response(resp) do
|
|
case resp do
|
|
"0x000000000000000000000000" <> address ->
|
|
"0x" <> address
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
def decode_contract_integer_response(resp) do
|
|
case resp do
|
|
"0x" <> integer_encoded ->
|
|
{integer_value, _} = Integer.parse(integer_encoded, 16)
|
|
integer_value
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Fetches a `t:Token.t/0` by an address hash.
|
|
|
|
## Options
|
|
|
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
|
|
`:required`, and the `t:Token.t/0` has no associated record for that association,
|
|
then the `t:Token.t/0` will not be included in the list.
|
|
"""
|
|
@spec token_from_address_hash(Hash.Address.t(), [necessity_by_association_option | api?]) ::
|
|
{:ok, Token.t()} | {:error, :not_found}
|
|
def token_from_address_hash(
|
|
%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash,
|
|
options \\ []
|
|
) do
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
query =
|
|
from(
|
|
t in Token,
|
|
where: t.contract_address_hash == ^hash,
|
|
select: t
|
|
)
|
|
|
|
query
|
|
|> join_associations(necessity_by_association)
|
|
|> preload(:contract_address)
|
|
|> select_repo(options).one()
|
|
|> case do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%Token{} = token ->
|
|
{:ok, token}
|
|
end
|
|
end
|
|
|
|
@spec token_from_address_hash_exists?(Hash.Address.t(), [api?]) :: boolean()
|
|
def token_from_address_hash_exists?(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, options) do
|
|
query =
|
|
from(
|
|
t in Token,
|
|
where: t.contract_address_hash == ^hash,
|
|
select: t
|
|
)
|
|
|
|
select_repo(options).exists?(query)
|
|
end
|
|
|
|
@spec fetch_token_transfers_from_token_hash(Hash.t(), [paging_options]) :: []
|
|
def fetch_token_transfers_from_token_hash(token_address_hash, options \\ []) do
|
|
TokenTransfer.fetch_token_transfers_from_token_hash(token_address_hash, options)
|
|
end
|
|
|
|
@spec fetch_token_transfers_from_token_hash_and_token_id(Hash.t(), non_neg_integer(), [paging_options]) :: []
|
|
def fetch_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options \\ []) do
|
|
TokenTransfer.fetch_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options)
|
|
end
|
|
|
|
@spec count_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
|
|
def count_token_transfers_from_token_hash(token_address_hash) do
|
|
TokenTransfer.count_token_transfers_from_token_hash(token_address_hash)
|
|
end
|
|
|
|
@spec count_token_transfers_from_token_hash_and_token_id(Hash.t(), non_neg_integer(), [api?]) :: non_neg_integer()
|
|
def count_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options \\ []) do
|
|
TokenTransfer.count_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options)
|
|
end
|
|
|
|
@spec transaction_has_token_transfers?(Hash.t()) :: boolean()
|
|
def transaction_has_token_transfers?(transaction_hash) do
|
|
query = from(tt in TokenTransfer, where: tt.transaction_hash == ^transaction_hash)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@spec address_has_rewards?(Address.t()) :: boolean()
|
|
def address_has_rewards?(address_hash) do
|
|
query = from(r in Reward, where: r.address_hash == ^address_hash)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@spec address_tokens_with_balance(Hash.Address.t(), [any()]) :: []
|
|
def address_tokens_with_balance(address_hash, paging_options \\ []) do
|
|
address_hash
|
|
|> Address.Token.list_address_tokens_with_balance(paging_options)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec find_and_update_replaced_transactions([
|
|
%{
|
|
required(:nonce) => non_neg_integer,
|
|
required(:from_address_hash) => Hash.Address.t(),
|
|
required(:hash) => Hash.t()
|
|
}
|
|
]) :: {integer(), nil | [term()]}
|
|
def find_and_update_replaced_transactions(transactions, timeout \\ :infinity) do
|
|
query =
|
|
transactions
|
|
|> Enum.reduce(
|
|
Transaction,
|
|
fn %{hash: hash, nonce: nonce, from_address_hash: from_address_hash}, query ->
|
|
from(t in query,
|
|
or_where:
|
|
t.nonce == ^nonce and t.from_address_hash == ^from_address_hash and t.hash != ^hash and
|
|
not is_nil(t.block_number)
|
|
)
|
|
end
|
|
)
|
|
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
|
|
|> order_by(asc: :hash)
|
|
|> lock("FOR NO KEY UPDATE")
|
|
|
|
hashes = Enum.map(transactions, & &1.hash)
|
|
|
|
transactions_to_update =
|
|
from(pending in Transaction,
|
|
join: duplicate in subquery(query),
|
|
on: duplicate.nonce == pending.nonce,
|
|
on: duplicate.from_address_hash == pending.from_address_hash,
|
|
where: pending.hash in ^hashes and is_nil(pending.block_hash)
|
|
)
|
|
|
|
Repo.update_all(transactions_to_update, [set: [error: "dropped/replaced", status: :error]], timeout: timeout)
|
|
end
|
|
|
|
@spec update_replaced_transactions([
|
|
%{
|
|
required(:nonce) => non_neg_integer,
|
|
required(:from_address_hash) => Hash.Address.t(),
|
|
required(:block_hash) => Hash.Full.t()
|
|
}
|
|
]) :: {integer(), nil | [term()]}
|
|
def update_replaced_transactions(transactions, timeout \\ :infinity) do
|
|
filters =
|
|
transactions
|
|
|> Enum.filter(fn transaction ->
|
|
transaction.block_hash && transaction.nonce && transaction.from_address_hash
|
|
end)
|
|
|> Enum.map(fn transaction ->
|
|
{transaction.nonce, transaction.from_address_hash}
|
|
end)
|
|
|> Enum.uniq()
|
|
|
|
if Enum.empty?(filters) do
|
|
{:ok, []}
|
|
else
|
|
query =
|
|
filters
|
|
|> Enum.reduce(Transaction, fn {nonce, from_address}, query ->
|
|
from(t in query,
|
|
or_where: t.nonce == ^nonce and t.from_address_hash == ^from_address and is_nil(t.block_hash)
|
|
)
|
|
end)
|
|
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
|
|
|> order_by(asc: :hash)
|
|
|> lock("FOR NO KEY UPDATE")
|
|
|
|
Repo.update_all(
|
|
from(t in Transaction, join: s in subquery(query), on: t.hash == s.hash),
|
|
[set: [error: "dropped/replaced", status: :error]],
|
|
timeout: timeout
|
|
)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Expects map of change params. Inserts using on_conflict: :replace_all
|
|
"""
|
|
@spec upsert_token_instance(map()) :: {:ok, Instance.t()} | {:error, Ecto.Changeset.t()}
|
|
def upsert_token_instance(params) do
|
|
changeset = Instance.changeset(%Instance{}, params)
|
|
|
|
Repo.insert(changeset,
|
|
on_conflict: :replace_all,
|
|
conflict_target: [:token_id, :token_contract_address_hash]
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Inserts list of token instances via upsert_token_instance/1.
|
|
"""
|
|
@spec upsert_token_instances_list([map()]) :: list()
|
|
def upsert_token_instances_list(instances) do
|
|
Enum.map(instances, &upsert_token_instance/1)
|
|
end
|
|
|
|
@doc """
|
|
Update a new `t:Token.t/0` record.
|
|
|
|
As part of updating token, an additional record is inserted for
|
|
naming the address for reference if a name is provided for a token.
|
|
"""
|
|
@spec update_token(Token.t(), map()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()}
|
|
def update_token(%Token{contract_address_hash: address_hash} = token, params \\ %{}) do
|
|
token_changeset = Token.changeset(token, Map.put(params, :updated_at, DateTime.utc_now()))
|
|
address_name_changeset = Address.Name.changeset(%Address.Name{}, Map.put(params, :address_hash, address_hash))
|
|
|
|
stale_error_field = :contract_address_hash
|
|
stale_error_message = "is up to date"
|
|
|
|
token_opts = [
|
|
on_conflict: Runner.Tokens.default_on_conflict(),
|
|
conflict_target: :contract_address_hash,
|
|
stale_error_field: stale_error_field,
|
|
stale_error_message: stale_error_message
|
|
]
|
|
|
|
address_name_opts = [on_conflict: :nothing, conflict_target: [:address_hash, :name]]
|
|
|
|
# Enforce ShareLocks tables order (see docs: sharelocks.md)
|
|
insert_result =
|
|
Multi.new()
|
|
|> Multi.run(
|
|
:address_name,
|
|
fn repo, _ ->
|
|
{:ok, repo.insert(address_name_changeset, address_name_opts)}
|
|
end
|
|
)
|
|
|> Multi.run(:token, fn repo, _ ->
|
|
with {:error, %Changeset{errors: [{^stale_error_field, {^stale_error_message, [_]}}]}} <-
|
|
repo.update(token_changeset, token_opts) do
|
|
# the original token passed into `update_token/2` as stale error means it is unchanged
|
|
{:ok, token}
|
|
end
|
|
end)
|
|
|> Repo.transaction()
|
|
|
|
case insert_result do
|
|
{:ok, %{token: token}} ->
|
|
{:ok, token}
|
|
|
|
{:error, :token, changeset, _} ->
|
|
{:error, changeset}
|
|
end
|
|
end
|
|
|
|
@spec fetch_last_token_balances_include_unfetched(Hash.Address.t(), [api?]) :: []
|
|
def fetch_last_token_balances_include_unfetched(address_hash, options \\ []) do
|
|
address_hash
|
|
|> CurrentTokenBalance.last_token_balances_include_unfetched()
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec fetch_last_token_balances(Hash.Address.t(), [api?]) :: []
|
|
def fetch_last_token_balances(address_hash, options \\ []) do
|
|
address_hash
|
|
|> CurrentTokenBalance.last_token_balances()
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec fetch_paginated_last_token_balances(Hash.Address.t(), [paging_options]) :: []
|
|
def fetch_paginated_last_token_balances(address_hash, options) do
|
|
filter = Keyword.get(options, :token_type)
|
|
options = Keyword.delete(options, :token_type)
|
|
|
|
address_hash
|
|
|> CurrentTokenBalance.last_token_balances(options, filter)
|
|
|> page_current_token_balances(options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
@spec erc721_or_erc1155_token_instance_from_token_id_and_token_address(non_neg_integer(), Hash.Address.t(), [api?]) ::
|
|
{:ok, Instance.t()} | {:error, :not_found}
|
|
def erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, token_contract_address, options \\ []) do
|
|
query = Instance.token_instance_query(token_id, token_contract_address)
|
|
|
|
case select_repo(options).one(query) do
|
|
nil -> {:error, :not_found}
|
|
token_instance -> {:ok, token_instance}
|
|
end
|
|
end
|
|
|
|
@spec token_instance_exists?(non_neg_integer, Hash.Address.t(), [api?]) :: boolean
|
|
def token_instance_exists?(token_id, token_contract_address, options \\ []) do
|
|
query = Instance.token_instance_query(token_id, token_contract_address)
|
|
|
|
select_repo(options).exists?(query)
|
|
end
|
|
|
|
defp fetch_coin_balances(address, paging_options) do
|
|
address.hash
|
|
|> CoinBalance.fetch_coin_balances(paging_options)
|
|
end
|
|
|
|
@spec fetch_last_token_balance(Hash.Address.t(), Hash.Address.t()) :: Decimal.t()
|
|
def fetch_last_token_balance(address_hash, token_contract_address_hash) do
|
|
if address_hash !== %{} do
|
|
address_hash
|
|
|> CurrentTokenBalance.last_token_balance(token_contract_address_hash) || Decimal.new(0)
|
|
else
|
|
Decimal.new(0)
|
|
end
|
|
end
|
|
|
|
# @spec fetch_last_token_balance_1155(Hash.Address.t(), Hash.Address.t()) :: Decimal.t()
|
|
def fetch_last_token_balance_1155(address_hash, token_contract_address_hash, token_id) do
|
|
if address_hash !== %{} do
|
|
address_hash
|
|
|> CurrentTokenBalance.last_token_balance_1155(token_contract_address_hash, token_id) || Decimal.new(0)
|
|
else
|
|
Decimal.new(0)
|
|
end
|
|
end
|
|
|
|
@spec address_to_coin_balances(Address.t(), [paging_options | api?]) :: []
|
|
def address_to_coin_balances(address, options) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
balances_raw =
|
|
address
|
|
|> fetch_coin_balances(paging_options)
|
|
|> page_coin_balances(paging_options)
|
|
|> select_repo(options).all()
|
|
|> preload_transactions(options)
|
|
|
|
if Enum.empty?(balances_raw) do
|
|
balances_raw
|
|
else
|
|
balances_raw_filtered =
|
|
balances_raw
|
|
|> Enum.filter(fn balance -> balance.value end)
|
|
|
|
min_block_number =
|
|
balances_raw_filtered
|
|
|> Enum.min_by(fn balance -> balance.block_number end, fn -> %{} end)
|
|
|> Map.get(:block_number)
|
|
|
|
max_block_number =
|
|
balances_raw_filtered
|
|
|> Enum.max_by(fn balance -> balance.block_number end, fn -> %{} end)
|
|
|> Map.get(:block_number)
|
|
|
|
min_block_timestamp = find_block_timestamp(min_block_number, options)
|
|
max_block_timestamp = find_block_timestamp(max_block_number, options)
|
|
|
|
min_block_unix_timestamp =
|
|
min_block_timestamp
|
|
|> Timex.to_unix()
|
|
|
|
max_block_unix_timestamp =
|
|
max_block_timestamp
|
|
|> Timex.to_unix()
|
|
|
|
blocks_delta = max_block_number - min_block_number
|
|
|
|
balances_with_dates =
|
|
if blocks_delta > 0 do
|
|
add_block_timestamp_to_balances(
|
|
balances_raw_filtered,
|
|
min_block_number,
|
|
min_block_unix_timestamp,
|
|
max_block_unix_timestamp,
|
|
blocks_delta
|
|
)
|
|
else
|
|
add_min_block_timestamp_to_balances(balances_raw_filtered, min_block_unix_timestamp)
|
|
end
|
|
|
|
balances_with_dates
|
|
|> Enum.sort(fn balance1, balance2 -> balance1.block_number >= balance2.block_number end)
|
|
end
|
|
end
|
|
|
|
# Here we fetch from DB one tx per one coin balance. It's much more faster than LEFT OUTER JOIN which was before.
|
|
defp preload_transactions(balances, options) do
|
|
tasks =
|
|
Enum.map(balances, fn balance ->
|
|
Task.async(fn ->
|
|
Transaction
|
|
|> where(
|
|
[tx],
|
|
tx.block_number == ^balance.block_number and tx.value > ^0 and
|
|
(tx.to_address_hash == ^balance.address_hash or tx.from_address_hash == ^balance.address_hash)
|
|
)
|
|
|> select([tx], tx.hash)
|
|
|> limit(1)
|
|
|> select_repo(options).one()
|
|
end)
|
|
end)
|
|
|
|
tasks
|
|
|> Task.yield_many(120_000)
|
|
|> Enum.zip(balances)
|
|
|> Enum.map(fn {{task, res}, balance} ->
|
|
case res do
|
|
{:ok, hash} ->
|
|
put_tx_hash(hash, balance)
|
|
|
|
{:exit, _reason} ->
|
|
balance
|
|
|
|
nil ->
|
|
Task.shutdown(task, :brutal_kill)
|
|
balance
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp put_tx_hash(hash, coin_balance),
|
|
do: if(hash, do: %CoinBalance{coin_balance | transaction_hash: hash}, else: coin_balance)
|
|
|
|
defp add_block_timestamp_to_balances(
|
|
balances_raw_filtered,
|
|
min_block_number,
|
|
min_block_unix_timestamp,
|
|
max_block_unix_timestamp,
|
|
blocks_delta
|
|
) do
|
|
balances_raw_filtered
|
|
|> Enum.map(fn balance ->
|
|
date =
|
|
trunc(
|
|
min_block_unix_timestamp +
|
|
(balance.block_number - min_block_number) * (max_block_unix_timestamp - min_block_unix_timestamp) /
|
|
blocks_delta
|
|
)
|
|
|
|
add_date_to_balance(balance, date)
|
|
end)
|
|
end
|
|
|
|
defp add_min_block_timestamp_to_balances(balances_raw_filtered, min_block_unix_timestamp) do
|
|
balances_raw_filtered
|
|
|> Enum.map(fn balance ->
|
|
date = min_block_unix_timestamp
|
|
|
|
add_date_to_balance(balance, date)
|
|
end)
|
|
end
|
|
|
|
defp add_date_to_balance(balance, date) do
|
|
formatted_date = Timex.from_unix(date)
|
|
%{balance | block_timestamp: formatted_date}
|
|
end
|
|
|
|
def get_token_balance(address_hash, token_contract_address_hash, block_number, token_id \\ nil, options \\ []) do
|
|
query = TokenBalance.fetch_token_balance(address_hash, token_contract_address_hash, block_number, token_id)
|
|
|
|
select_repo(options).one(query)
|
|
end
|
|
|
|
def get_coin_balance(address_hash, block_number, options \\ []) do
|
|
query = CoinBalance.fetch_coin_balance(address_hash, block_number)
|
|
|
|
select_repo(options).one(query)
|
|
end
|
|
|
|
@spec address_to_balances_by_day(Hash.Address.t(), [api?]) :: [balance_by_day]
|
|
def address_to_balances_by_day(address_hash, options \\ []) do
|
|
latest_block_timestamp =
|
|
address_hash
|
|
|> CoinBalance.last_coin_balance_timestamp()
|
|
|> select_repo(options).one()
|
|
|
|
address_hash
|
|
|> CoinBalanceDaily.balances_by_day()
|
|
|> select_repo(options).all()
|
|
|> Enum.sort_by(fn %{date: d} -> {d.year, d.month, d.day} end)
|
|
|> replace_last_value(latest_block_timestamp)
|
|
|> normalize_balances_by_day(Keyword.get(options, :api?, false))
|
|
end
|
|
|
|
# https://github.com/blockscout/blockscout/issues/2658
|
|
defp replace_last_value(items, %{value: value, timestamp: timestamp}) do
|
|
List.replace_at(items, -1, %{date: Date.convert!(timestamp, Calendar.ISO), value: value})
|
|
end
|
|
|
|
defp replace_last_value(items, _), do: items
|
|
|
|
defp normalize_balances_by_day(balances_by_day, api?) do
|
|
result =
|
|
balances_by_day
|
|
|> Enum.filter(fn day -> day.value end)
|
|
|> (&if(api?, do: &1, else: Enum.map(&1, fn day -> Map.update!(day, :date, fn x -> to_string(x) end) end))).()
|
|
|> (&if(api?, do: &1, else: Enum.map(&1, fn day -> Map.update!(day, :value, fn x -> Wei.to(x, :ether) end) end))).()
|
|
|
|
today = Date.to_string(NaiveDateTime.utc_now())
|
|
|
|
if Enum.count(result) > 0 && !Enum.any?(result, fn map -> map[:date] == today end) do
|
|
List.flatten([result | [%{date: today, value: List.last(result)[:value]}]])
|
|
else
|
|
result
|
|
end
|
|
end
|
|
|
|
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options | api?]) :: [TokenBalance.t()]
|
|
def fetch_token_holders_from_token_hash(contract_address_hash, options \\ []) do
|
|
query =
|
|
contract_address_hash
|
|
|> CurrentTokenBalance.token_holders_ordered_by_value(options)
|
|
|
|
query
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
def fetch_token_holders_from_token_hash_and_token_id(contract_address_hash, token_id, options \\ []) do
|
|
contract_address_hash
|
|
|> CurrentTokenBalance.token_holders_1155_by_token_id(token_id, options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
def token_id_1155_is_unique?(contract_address_hash, token_id, options \\ [])
|
|
|
|
def token_id_1155_is_unique?(_, nil, _), do: false
|
|
|
|
def token_id_1155_is_unique?(contract_address_hash, token_id, options) do
|
|
result =
|
|
contract_address_hash |> CurrentTokenBalance.token_balances_by_id_limit_2(token_id) |> select_repo(options).all()
|
|
|
|
if length(result) == 1 do
|
|
Decimal.compare(Enum.at(result, 0), 1) == :eq
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def get_token_ids_1155(contract_address_hash) do
|
|
contract_address_hash
|
|
|> CurrentTokenBalance.token_ids_query()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec count_token_holders_from_token_hash(Hash.Address.t()) :: non_neg_integer()
|
|
def count_token_holders_from_token_hash(contract_address_hash) do
|
|
query =
|
|
from(ctb in CurrentTokenBalance.token_holders_query_for_count(contract_address_hash),
|
|
select: fragment("COUNT(DISTINCT(?))", ctb.address_hash)
|
|
)
|
|
|
|
Repo.one!(query, timeout: :infinity)
|
|
end
|
|
|
|
@spec address_to_unique_tokens(Hash.Address.t(), [paging_options | api?]) :: [Instance.t()]
|
|
def address_to_unique_tokens(contract_address_hash, options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
|
|
contract_address_hash
|
|
|> Instance.address_to_unique_token_instances()
|
|
|> Instance.page_token_instance(paging_options)
|
|
|> limit(^paging_options.page_size)
|
|
|> select_repo(options).all()
|
|
|> Enum.map(&put_owner_to_token_instance(&1, options))
|
|
end
|
|
|
|
def put_owner_to_token_instance(%Instance{} = token_instance, options \\ []) do
|
|
owner =
|
|
token_instance
|
|
|> Instance.owner_query()
|
|
|> select_repo(options).one()
|
|
|
|
%{token_instance | owner: owner}
|
|
end
|
|
|
|
@spec data() :: Dataloader.Ecto.t()
|
|
def data, do: DataloaderEcto.new(Repo)
|
|
|
|
@spec transaction_token_transfer_type(Transaction.t()) ::
|
|
:erc20 | :erc721 | :erc1155 | :token_transfer | nil
|
|
def transaction_token_transfer_type(
|
|
%Transaction{
|
|
status: :ok,
|
|
created_contract_address_hash: nil,
|
|
input: input,
|
|
value: value
|
|
} = transaction
|
|
) do
|
|
zero_wei = %Wei{value: Decimal.new(0)}
|
|
result = find_token_transfer_type(transaction, input, value)
|
|
|
|
if is_nil(result) && Enum.count(transaction.token_transfers) > 0 && value == zero_wei,
|
|
do: :token_transfer,
|
|
else: result
|
|
rescue
|
|
_ -> nil
|
|
end
|
|
|
|
def transaction_token_transfer_type(_), do: nil
|
|
|
|
defp find_token_transfer_type(transaction, input, value) do
|
|
zero_wei = %Wei{value: Decimal.new(0)}
|
|
|
|
# https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC721/ERC721.sol#L35
|
|
case {to_string(input), value} do
|
|
# transferFrom(address,address,uint256)
|
|
{"0x23b872dd" <> params, ^zero_wei} ->
|
|
types = [:address, :address, {:uint, 256}]
|
|
[from_address, to_address, _value] = decode_params(params, types)
|
|
|
|
find_erc721_token_transfer(transaction.token_transfers, {from_address, to_address})
|
|
|
|
# safeTransferFrom(address,address,uint256)
|
|
{"0x42842e0e" <> params, ^zero_wei} ->
|
|
types = [:address, :address, {:uint, 256}]
|
|
[from_address, to_address, _value] = decode_params(params, types)
|
|
|
|
find_erc721_token_transfer(transaction.token_transfers, {from_address, to_address})
|
|
|
|
# safeTransferFrom(address,address,uint256,bytes)
|
|
{"0xb88d4fde" <> params, ^zero_wei} ->
|
|
types = [:address, :address, {:uint, 256}, :bytes]
|
|
[from_address, to_address, _value, _data] = decode_params(params, types)
|
|
|
|
find_erc721_token_transfer(transaction.token_transfers, {from_address, to_address})
|
|
|
|
# safeTransferFrom(address,address,uint256,uint256,bytes)
|
|
{"0xf242432a" <> params, ^zero_wei} ->
|
|
types = [:address, :address, {:uint, 256}, {:uint, 256}, :bytes]
|
|
[from_address, to_address, _id, _value, _data] = decode_params(params, types)
|
|
|
|
find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address})
|
|
|
|
# safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)
|
|
{"0x2eb2c2d6" <> params, ^zero_wei} ->
|
|
types = [:address, :address, [{:uint, 256}], [{:uint, 256}], :bytes]
|
|
[from_address, to_address, _ids, _values, _data] = decode_params(params, types)
|
|
|
|
find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address})
|
|
|
|
{"0xf907fc5b" <> _params, ^zero_wei} ->
|
|
:erc20
|
|
|
|
# check for ERC-20 or for old ERC-721, ERC-1155 token versions
|
|
{unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} ->
|
|
types = [:address, {:uint, 256}]
|
|
|
|
[address, value] = decode_params(params, types)
|
|
|
|
decimal_value = Decimal.new(value)
|
|
|
|
find_erc721_or_erc20_or_erc1155_token_transfer(transaction.token_transfers, {address, decimal_value})
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp find_erc721_token_transfer(token_transfers, {from_address, to_address}) do
|
|
token_transfer =
|
|
Enum.find(token_transfers, fn token_transfer ->
|
|
token_transfer.from_address_hash.bytes == from_address && token_transfer.to_address_hash.bytes == to_address
|
|
end)
|
|
|
|
if token_transfer, do: :erc721
|
|
end
|
|
|
|
defp find_erc1155_token_transfer(token_transfers, {from_address, to_address}) do
|
|
token_transfer =
|
|
Enum.find(token_transfers, fn token_transfer ->
|
|
token_transfer.from_address_hash.bytes == from_address && token_transfer.to_address_hash.bytes == to_address
|
|
end)
|
|
|
|
if token_transfer, do: :erc1155
|
|
end
|
|
|
|
defp find_erc721_or_erc20_or_erc1155_token_transfer(token_transfers, {address, decimal_value}) do
|
|
token_transfer =
|
|
Enum.find(token_transfers, fn token_transfer ->
|
|
token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value
|
|
end)
|
|
|
|
if token_transfer do
|
|
case token_transfer.token do
|
|
%Token{type: "ERC-20"} -> :erc20
|
|
%Token{type: "ERC-721"} -> :erc721
|
|
%Token{type: "ERC-1155"} -> :erc1155
|
|
_ -> nil
|
|
end
|
|
else
|
|
:erc20
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Combined block reward from all the fees.
|
|
"""
|
|
@spec block_combined_rewards(Block.t()) :: Wei.t()
|
|
def block_combined_rewards(block) do
|
|
{:ok, value} =
|
|
block.rewards
|
|
|> Enum.reduce(
|
|
0,
|
|
fn block_reward, acc ->
|
|
{:ok, decimal} = Wei.dump(block_reward.reward)
|
|
|
|
Decimal.add(decimal, acc)
|
|
end
|
|
)
|
|
|> Wei.cast()
|
|
|
|
value
|
|
end
|
|
|
|
defp with_decompiled_code_flag(query, _hash, false), do: query
|
|
|
|
defp with_decompiled_code_flag(query, hash, true) do
|
|
has_decompiled_code_query =
|
|
from(decompiled_contract in DecompiledSmartContract,
|
|
where: decompiled_contract.address_hash == ^hash,
|
|
limit: 1,
|
|
select: %{
|
|
address_hash: decompiled_contract.address_hash,
|
|
has_decompiled_code?: not is_nil(decompiled_contract.address_hash)
|
|
}
|
|
)
|
|
|
|
from(
|
|
address in query,
|
|
left_join: decompiled_code in subquery(has_decompiled_code_query),
|
|
on: address.hash == decompiled_code.address_hash,
|
|
select_merge: %{has_decompiled_code?: decompiled_code.has_decompiled_code?}
|
|
)
|
|
end
|
|
|
|
defp decode_params(params, types) do
|
|
params
|
|
|> Base.decode16!(case: :mixed)
|
|
|> TypeDecoder.decode_raw(types)
|
|
end
|
|
|
|
@spec get_token_type(Hash.Address.t()) :: String.t() | nil
|
|
def get_token_type(hash) do
|
|
query =
|
|
from(
|
|
token in Token,
|
|
where: token.contract_address_hash == ^hash,
|
|
select: token.type
|
|
)
|
|
|
|
Repo.one(query)
|
|
end
|
|
|
|
@spec is_erc_20_token?(Token.t()) :: bool
|
|
def is_erc_20_token?(token) do
|
|
is_erc_20_token_type?(token.type)
|
|
end
|
|
|
|
defp is_erc_20_token_type?(type) do
|
|
case type do
|
|
"ERC-20" -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if an `t:Explorer.Chain.Address.t/0` with the given `hash` exists.
|
|
|
|
Returns `:ok` if found
|
|
|
|
iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}
|
|
...> )
|
|
iex> Explorer.Chain.check_address_exists(hash)
|
|
:ok
|
|
|
|
Returns `:not_found` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.check_address_exists(hash)
|
|
:not_found
|
|
|
|
"""
|
|
@spec check_address_exists(Hash.Address.t()) :: :ok | :not_found
|
|
def check_address_exists(address_hash) do
|
|
address_hash
|
|
|> address_exists?()
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if an `t:Explorer.Chain.Address.t/0` with the given `hash` exists.
|
|
|
|
Returns `true` if found
|
|
|
|
iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address(
|
|
...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}
|
|
...> )
|
|
iex> Explorer.Chain.address_exists?(hash)
|
|
true
|
|
|
|
Returns `false` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.address_exists?(hash)
|
|
false
|
|
|
|
"""
|
|
@spec address_exists?(Hash.Address.t()) :: boolean()
|
|
def address_exists?(address_hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^address_hash
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Checks if it exists an `t:Explorer.Chain.Address.t/0` that has the provided
|
|
`t:Explorer.Chain.Address.t/0` `hash` and a contract.
|
|
|
|
Returns `:ok` if found and `:not_found` otherwise.
|
|
"""
|
|
@spec check_contract_address_exists(Hash.Address.t()) :: :ok | :not_found
|
|
def check_contract_address_exists(address_hash) do
|
|
address_hash
|
|
|> contract_address_exists?()
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if it exists an `t:Explorer.Chain.Address.t/0` that has the provided
|
|
`t:Explorer.Chain.Address.t/0` `hash` and a contract.
|
|
|
|
Returns `true` if found and `false` otherwise.
|
|
"""
|
|
@spec contract_address_exists?(Hash.Address.t()) :: boolean()
|
|
def contract_address_exists?(address_hash) do
|
|
query =
|
|
from(
|
|
address in Address,
|
|
where: address.hash == ^address_hash and not is_nil(address.contract_code)
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Checks if it exists a `t:Explorer.Chain.DecompiledSmartContract.t/0` for the
|
|
`t:Explorer.Chain.Address.t/0` with the provided `hash` and with the provided version.
|
|
|
|
Returns `:ok` if found and `:not_found` otherwise.
|
|
"""
|
|
@spec check_decompiled_contract_exists(Hash.Address.t(), String.t()) :: :ok | :not_found
|
|
def check_decompiled_contract_exists(address_hash, version) do
|
|
address_hash
|
|
|> decompiled_contract_exists?(version)
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if it exists a `t:Explorer.Chain.DecompiledSmartContract.t/0` for the
|
|
`t:Explorer.Chain.Address.t/0` with the provided `hash` and with the provided version.
|
|
|
|
Returns `true` if found and `false` otherwise.
|
|
"""
|
|
@spec decompiled_contract_exists?(Hash.Address.t(), String.t()) :: boolean()
|
|
def decompiled_contract_exists?(address_hash, version) do
|
|
query =
|
|
from(contract in DecompiledSmartContract,
|
|
where: contract.address_hash == ^address_hash and contract.decompiler_version == ^version
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the
|
|
`t:Explorer.Chain.Address.t/0` with the provided `hash`.
|
|
|
|
Returns `:ok` if found and `:not_found` otherwise.
|
|
"""
|
|
@spec check_verified_smart_contract_exists(Hash.Address.t()) :: :ok | :not_found
|
|
def check_verified_smart_contract_exists(address_hash) do
|
|
address_hash
|
|
|> verified_smart_contract_exists?()
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the
|
|
`t:Explorer.Chain.Address.t/0` with the provided `hash`.
|
|
|
|
Returns `true` if found and `false` otherwise.
|
|
"""
|
|
@spec verified_smart_contract_exists?(Hash.Address.t()) :: boolean()
|
|
def verified_smart_contract_exists?(address_hash) do
|
|
query =
|
|
from(
|
|
smart_contract in SmartContract,
|
|
where: smart_contract.address_hash == ^address_hash
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists.
|
|
|
|
Returns `:ok` if found
|
|
|
|
iex> %Transaction{hash: hash} = insert(:transaction)
|
|
iex> Explorer.Chain.check_transaction_exists(hash)
|
|
:ok
|
|
|
|
Returns `:not_found` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
iex> Explorer.Chain.check_transaction_exists(hash)
|
|
:not_found
|
|
"""
|
|
@spec check_transaction_exists(Hash.Full.t()) :: :ok | :not_found
|
|
def check_transaction_exists(hash) do
|
|
hash
|
|
|> transaction_exists?()
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists.
|
|
|
|
Returns `true` if found
|
|
|
|
iex> %Transaction{hash: hash} = insert(:transaction)
|
|
iex> Explorer.Chain.transaction_exists?(hash)
|
|
true
|
|
|
|
Returns `false` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash(
|
|
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
|
|
...> )
|
|
iex> Explorer.Chain.transaction_exists?(hash)
|
|
false
|
|
"""
|
|
@spec transaction_exists?(Hash.Full.t()) :: boolean()
|
|
def transaction_exists?(hash) do
|
|
query =
|
|
from(
|
|
transaction in Transaction,
|
|
where: transaction.hash == ^hash
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists.
|
|
|
|
Returns `:ok` if found
|
|
|
|
iex> address = insert(:address)
|
|
iex> insert(:token, contract_address: address)
|
|
iex> Explorer.Chain.check_token_exists(address.hash)
|
|
:ok
|
|
|
|
Returns `:not_found` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.check_token_exists(hash)
|
|
:not_found
|
|
"""
|
|
@spec check_token_exists(Hash.Address.t()) :: :ok | :not_found
|
|
def check_token_exists(hash) do
|
|
hash
|
|
|> token_exists?()
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists.
|
|
|
|
Returns `true` if found
|
|
|
|
iex> address = insert(:address)
|
|
iex> insert(:token, contract_address: address)
|
|
iex> Explorer.Chain.token_exists?(address.hash)
|
|
true
|
|
|
|
Returns `false` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.token_exists?(hash)
|
|
false
|
|
"""
|
|
@spec token_exists?(Hash.Address.t()) :: boolean()
|
|
def token_exists?(hash) do
|
|
query =
|
|
from(
|
|
token in Token,
|
|
where: token.contract_address_hash == ^hash
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Checks if a `t:Explorer.Chain.Token.Instance.t/0` with the given `hash` and `token_id` exists.
|
|
|
|
Returns `:ok` if found
|
|
|
|
iex> token = insert(:token)
|
|
iex> token_id = 10
|
|
iex> insert(:token_instance,
|
|
...> token_contract_address_hash: token.contract_address_hash,
|
|
...> token_id: token_id
|
|
...> )
|
|
iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(token_id, token.contract_address_hash)
|
|
:ok
|
|
|
|
Returns `:not_found` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(10, hash)
|
|
:not_found
|
|
"""
|
|
@spec check_erc721_or_erc1155_token_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) ::
|
|
:ok | :not_found
|
|
def check_erc721_or_erc1155_token_instance_exists(token_id, hash) do
|
|
token_id
|
|
|> erc721_or_erc1155_token_instance_exist?(hash)
|
|
|> boolean_to_check_result()
|
|
end
|
|
|
|
@doc """
|
|
Checks if a `t:Explorer.Chain.Token.Instance.t/0` with the given `hash` and `token_id` exists.
|
|
|
|
Returns `true` if found
|
|
|
|
iex> token = insert(:token)
|
|
iex> token_id = 10
|
|
iex> insert(:token_instance,
|
|
...> token_contract_address_hash: token.contract_address_hash,
|
|
...> token_id: token_id
|
|
...> )
|
|
iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(token_id, token.contract_address_hash)
|
|
true
|
|
|
|
Returns `false` if not found
|
|
|
|
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
|
|
iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(10, hash)
|
|
false
|
|
"""
|
|
@spec erc721_or_erc1155_token_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean()
|
|
def erc721_or_erc1155_token_instance_exist?(token_id, hash) do
|
|
query =
|
|
from(i in Instance,
|
|
where: i.token_contract_address_hash == ^hash and i.token_id == ^Decimal.new(token_id)
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
defp boolean_to_check_result(true), do: :ok
|
|
|
|
defp boolean_to_check_result(false), do: :not_found
|
|
|
|
@doc """
|
|
Fetches the first trace from the Nethermind trace URL.
|
|
"""
|
|
def fetch_first_trace(transactions_params, json_rpc_named_arguments) do
|
|
case EthereumJSONRPC.fetch_first_trace(transactions_params, json_rpc_named_arguments) do
|
|
{:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} ->
|
|
format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments)
|
|
|
|
{:error, error} ->
|
|
{:error, error}
|
|
|
|
:ignore ->
|
|
:ignore
|
|
end
|
|
end
|
|
|
|
def combine_proxy_implementation_abi(smart_contract, options \\ [])
|
|
|
|
def combine_proxy_implementation_abi(%SmartContract{abi: abi} = smart_contract, options) when not is_nil(abi) do
|
|
implementation_abi = get_implementation_abi_from_proxy(smart_contract, options)
|
|
|
|
if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi
|
|
end
|
|
|
|
def combine_proxy_implementation_abi(_, _) do
|
|
[]
|
|
end
|
|
|
|
def gnosis_safe_contract?(abi) when not is_nil(abi) do
|
|
implementation_method_abi =
|
|
abi
|
|
|> Enum.find(fn method ->
|
|
master_copy_pattern?(method)
|
|
end)
|
|
|
|
if implementation_method_abi, do: true, else: false
|
|
end
|
|
|
|
def gnosis_safe_contract?(abi) when is_nil(abi), do: false
|
|
|
|
def master_copy_pattern?(method) do
|
|
Map.get(method, "type") == "constructor" &&
|
|
method
|
|
|> Enum.find(fn item ->
|
|
case item do
|
|
{"inputs", inputs} ->
|
|
master_copy_input?(inputs)
|
|
|
|
_ ->
|
|
false
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp master_copy_input?(inputs) do
|
|
inputs
|
|
|> Enum.find(fn input ->
|
|
Map.get(input, "name") == "_masterCopy"
|
|
end)
|
|
end
|
|
|
|
def get_implementation_abi(implementation_address_hash_string, options \\ [])
|
|
|
|
def get_implementation_abi(implementation_address_hash_string, options)
|
|
when not is_nil(implementation_address_hash_string) do
|
|
case Chain.string_to_address_hash(implementation_address_hash_string) do
|
|
{:ok, implementation_address_hash} ->
|
|
implementation_smart_contract =
|
|
implementation_address_hash
|
|
|> address_hash_to_smart_contract(options)
|
|
|
|
if implementation_smart_contract do
|
|
implementation_smart_contract
|
|
|> Map.get(:abi)
|
|
else
|
|
[]
|
|
end
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
def get_implementation_abi(implementation_address_hash_string, _) when is_nil(implementation_address_hash_string) do
|
|
[]
|
|
end
|
|
|
|
def get_implementation_abi_from_proxy(
|
|
%SmartContract{address_hash: proxy_address_hash, abi: abi} = smart_contract,
|
|
options
|
|
)
|
|
when not is_nil(proxy_address_hash) and not is_nil(abi) do
|
|
{implementation_address_hash_string, _name} = SmartContract.get_implementation_address_hash(smart_contract, options)
|
|
get_implementation_abi(implementation_address_hash_string)
|
|
end
|
|
|
|
def get_implementation_abi_from_proxy(_, _), do: []
|
|
|
|
defp format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) do
|
|
{:ok, to_address_hash} =
|
|
if Map.has_key?(first_trace, :to_address_hash) do
|
|
Chain.string_to_address_hash(first_trace.to_address_hash)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
{:ok, from_address_hash} = Chain.string_to_address_hash(first_trace.from_address_hash)
|
|
|
|
{:ok, created_contract_address_hash} =
|
|
if Map.has_key?(first_trace, :created_contract_address_hash) do
|
|
Chain.string_to_address_hash(first_trace.created_contract_address_hash)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
{:ok, transaction_hash} = Chain.string_to_transaction_hash(first_trace.transaction_hash)
|
|
|
|
{:ok, call_type} =
|
|
if Map.has_key?(first_trace, :call_type) do
|
|
CallType.load(first_trace.call_type)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
{:ok, type} = Type.load(first_trace.type)
|
|
|
|
{:ok, input} =
|
|
if Map.has_key?(first_trace, :input) do
|
|
Data.cast(first_trace.input)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
{:ok, output} =
|
|
if Map.has_key?(first_trace, :output) do
|
|
Data.cast(first_trace.output)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
{:ok, created_contract_code} =
|
|
if Map.has_key?(first_trace, :created_contract_code) do
|
|
Data.cast(first_trace.created_contract_code)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
{:ok, init} =
|
|
if Map.has_key?(first_trace, :init) do
|
|
Data.cast(first_trace.init)
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
block_index =
|
|
get_block_index(%{
|
|
transaction_index: first_trace.transaction_index,
|
|
transaction_hash: first_trace.transaction_hash,
|
|
block_number: first_trace.block_number,
|
|
json_rpc_named_arguments: json_rpc_named_arguments
|
|
})
|
|
|
|
value = %Wei{value: Decimal.new(first_trace.value)}
|
|
|
|
first_trace_formatted =
|
|
first_trace
|
|
|> Map.merge(%{
|
|
block_index: block_index,
|
|
block_hash: block_hash,
|
|
call_type: call_type,
|
|
to_address_hash: to_address_hash,
|
|
created_contract_address_hash: created_contract_address_hash,
|
|
from_address_hash: from_address_hash,
|
|
input: input,
|
|
output: output,
|
|
created_contract_code: created_contract_code,
|
|
init: init,
|
|
transaction_hash: transaction_hash,
|
|
type: type,
|
|
value: value
|
|
})
|
|
|
|
{:ok, [first_trace_formatted]}
|
|
end
|
|
|
|
defp get_block_index(%{
|
|
transaction_index: transaction_index,
|
|
transaction_hash: transaction_hash,
|
|
block_number: block_number,
|
|
json_rpc_named_arguments: json_rpc_named_arguments
|
|
}) do
|
|
if transaction_index == 0 do
|
|
0
|
|
else
|
|
filtered_block_numbers = EthereumJSONRPC.block_numbers_in_range([block_number])
|
|
{:ok, traces} = fetch_block_internal_transactions(filtered_block_numbers, json_rpc_named_arguments)
|
|
|
|
sorted_traces =
|
|
traces
|
|
|> Enum.sort_by(&{&1.transaction_index, &1.index})
|
|
|> Enum.with_index()
|
|
|
|
{_, block_index} =
|
|
sorted_traces
|
|
|> Enum.find({nil, -1}, fn {trace, _} ->
|
|
trace.transaction_index == transaction_index &&
|
|
trace.transaction_hash == transaction_hash
|
|
end)
|
|
|
|
block_index
|
|
end
|
|
end
|
|
|
|
defp find_block_timestamp(number, options) do
|
|
Block
|
|
|> where([block], block.number == ^number)
|
|
|> select([block], block.timestamp)
|
|
|> limit(1)
|
|
|> select_repo(options).one()
|
|
end
|
|
|
|
@spec get_token_transfer_type(TokenTransfer.t()) ::
|
|
:token_burning | :token_minting | :token_spawning | :token_transfer
|
|
def get_token_transfer_type(transfer) do
|
|
{:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string())
|
|
|
|
cond do
|
|
transfer.to_address_hash == burn_address_hash && transfer.from_address_hash !== burn_address_hash ->
|
|
:token_burning
|
|
|
|
transfer.to_address_hash !== burn_address_hash && transfer.from_address_hash == burn_address_hash ->
|
|
:token_minting
|
|
|
|
transfer.to_address_hash == burn_address_hash && transfer.from_address_hash == burn_address_hash ->
|
|
:token_spawning
|
|
|
|
true ->
|
|
:token_transfer
|
|
end
|
|
end
|
|
|
|
@spec get_token_icon_url_by(String.t(), String.t()) :: String.t() | nil
|
|
def get_token_icon_url_by(chain_id, address_hash) do
|
|
chain_name =
|
|
case chain_id do
|
|
"1" ->
|
|
"ethereum"
|
|
|
|
"99" ->
|
|
"poa"
|
|
|
|
"100" ->
|
|
"xdai"
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
|
|
if chain_name do
|
|
try_url =
|
|
"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/#{chain_name}/assets/#{address_hash}/logo.png"
|
|
|
|
try_url
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp from_block(options) do
|
|
Keyword.get(options, :from_block) || nil
|
|
end
|
|
|
|
def to_block(options) do
|
|
Keyword.get(options, :to_block) || nil
|
|
end
|
|
|
|
def convert_date_to_min_block(date_str) do
|
|
date_format = "%Y-%m-%d"
|
|
|
|
{:ok, date} =
|
|
date_str
|
|
|> Timex.parse(date_format, :strftime)
|
|
|
|
{:ok, day_before} =
|
|
date
|
|
|> Timex.shift(days: -1)
|
|
|> Timex.format(date_format, :strftime)
|
|
|
|
convert_date_to_max_block(day_before)
|
|
end
|
|
|
|
def convert_date_to_max_block(date) do
|
|
query =
|
|
from(block in Block,
|
|
where: fragment("DATE(timestamp) = TO_DATE(?, 'YYYY-MM-DD')", ^date),
|
|
select: max(block.number)
|
|
)
|
|
|
|
query
|
|
|> Repo.one()
|
|
end
|
|
|
|
def is_address_hash_is_smart_contract?(nil), do: false
|
|
|
|
def is_address_hash_is_smart_contract?(address_hash) do
|
|
with %Address{contract_code: bytecode} <- Repo.get_by(Address, hash: address_hash),
|
|
false <- is_nil(bytecode) do
|
|
true
|
|
else
|
|
_ ->
|
|
false
|
|
end
|
|
end
|
|
|
|
def hash_to_lower_case_string(hash) do
|
|
hash
|
|
|> to_string()
|
|
|> String.downcase()
|
|
end
|
|
|
|
def recent_transactions(options, [:pending | _]) do
|
|
recent_pending_transactions(options, false)
|
|
end
|
|
|
|
def recent_transactions(options, _) do
|
|
recent_collated_transactions(false, options)
|
|
end
|
|
|
|
def apply_filter_by_method_id_to_transactions(query, nil), do: query
|
|
|
|
def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do
|
|
method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1)
|
|
|
|
if method_ids != [] do
|
|
query
|
|
|> where([tx], fragment("SUBSTRING(? FOR 4)", tx.input) in ^method_ids)
|
|
else
|
|
query
|
|
end
|
|
end
|
|
|
|
def apply_filter_by_method_id_to_transactions(query, filter),
|
|
do: apply_filter_by_method_id_to_transactions(query, [filter])
|
|
|
|
defp map_name_or_method_id_to_method_id(string) when is_binary(string) do
|
|
if id = @method_name_to_id_map[string] do
|
|
decode_method_id(id)
|
|
else
|
|
trimmed =
|
|
string
|
|
|> String.replace("0x", "", global: false)
|
|
|
|
decode_method_id(trimmed)
|
|
end
|
|
end
|
|
|
|
defp decode_method_id(method_id) when is_binary(method_id) do
|
|
case String.length(method_id) == 8 && Base.decode16(method_id, case: :mixed) do
|
|
{:ok, bytes} ->
|
|
[bytes]
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
def apply_filter_by_tx_type_to_transactions(query, [_ | _] = filter) do
|
|
{dynamic, modified_query} = apply_filter_by_tx_type_to_transactions_inner(filter, query)
|
|
|
|
modified_query
|
|
|> where(^dynamic)
|
|
end
|
|
|
|
def apply_filter_by_tx_type_to_transactions(query, _filter), do: query
|
|
|
|
def apply_filter_by_tx_type_to_transactions_inner(dynamic \\ dynamic(false), filter, query)
|
|
|
|
def apply_filter_by_tx_type_to_transactions_inner(dynamic, [type | remain], query) do
|
|
case type do
|
|
:contract_call ->
|
|
dynamic
|
|
|> filter_contract_call_dynamic()
|
|
|> apply_filter_by_tx_type_to_transactions_inner(
|
|
remain,
|
|
join(query, :inner, [tx], address in assoc(tx, :to_address), as: :to_address)
|
|
)
|
|
|
|
:contract_creation ->
|
|
dynamic
|
|
|> filter_contract_creation_dynamic()
|
|
|> apply_filter_by_tx_type_to_transactions_inner(remain, query)
|
|
|
|
:coin_transfer ->
|
|
dynamic
|
|
|> filter_transaction_dynamic()
|
|
|> apply_filter_by_tx_type_to_transactions_inner(remain, query)
|
|
|
|
:token_transfer ->
|
|
dynamic
|
|
|> filter_token_transfer_dynamic()
|
|
|> apply_filter_by_tx_type_to_transactions_inner(remain, query)
|
|
|
|
:token_creation ->
|
|
dynamic
|
|
|> filter_token_creation_dynamic()
|
|
|> apply_filter_by_tx_type_to_transactions_inner(
|
|
remain,
|
|
join(query, :inner, [tx], token in Token,
|
|
on: token.contract_address_hash == tx.created_contract_address_hash,
|
|
as: :created_token
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
def apply_filter_by_tx_type_to_transactions_inner(dynamic_query, _, query), do: {dynamic_query, query}
|
|
|
|
def filter_contract_creation_dynamic(dynamic) do
|
|
dynamic([tx], ^dynamic or is_nil(tx.to_address_hash))
|
|
end
|
|
|
|
def filter_transaction_dynamic(dynamic) do
|
|
dynamic([tx], ^dynamic or tx.value > ^0)
|
|
end
|
|
|
|
def filter_contract_call_dynamic(dynamic) do
|
|
dynamic([tx, to_address: to_address], ^dynamic or not is_nil(to_address.contract_code))
|
|
end
|
|
|
|
def filter_token_transfer_dynamic(dynamic) do
|
|
# TokenTransfer.__struct__.__meta__.source
|
|
dynamic(
|
|
[tx],
|
|
^dynamic or
|
|
fragment(
|
|
"NOT (SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NULL",
|
|
tx.hash
|
|
)
|
|
)
|
|
end
|
|
|
|
def filter_token_creation_dynamic(dynamic) do
|
|
dynamic([tx, created_token: created_token], ^dynamic or not is_nil(created_token))
|
|
end
|
|
|
|
@spec verified_contracts([
|
|
paging_options
|
|
| necessity_by_association_option
|
|
| {:filter, :solidity | :vyper}
|
|
| {:search, String.t() | {:api?, true | false}}
|
|
]) :: [SmartContract.t()]
|
|
def verified_contracts(options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
filter = Keyword.get(options, :filter, nil)
|
|
search_string = Keyword.get(options, :search, nil)
|
|
|
|
query = from(contract in SmartContract, select: contract, order_by: [desc: :id])
|
|
|
|
query
|
|
|> filter_contracts(filter)
|
|
|> search_contracts(search_string)
|
|
|> handle_verified_contracts_paging_options(paging_options)
|
|
|> join_associations(necessity_by_association)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
defp search_contracts(basic_query, nil), do: basic_query
|
|
|
|
defp search_contracts(basic_query, search_string) do
|
|
from(contract in basic_query,
|
|
where:
|
|
ilike(contract.name, ^"%#{search_string}%") or
|
|
ilike(fragment("'0x' || encode(?, 'hex')", contract.address_hash), ^"%#{search_string}%")
|
|
)
|
|
end
|
|
|
|
defp filter_contracts(basic_query, :solidity) do
|
|
basic_query
|
|
|> where(is_vyper_contract: ^false)
|
|
end
|
|
|
|
defp filter_contracts(basic_query, :vyper) do
|
|
basic_query
|
|
|> where(is_vyper_contract: ^true)
|
|
end
|
|
|
|
defp filter_contracts(basic_query, :yul) do
|
|
from(query in basic_query, where: is_nil(query.abi))
|
|
end
|
|
|
|
defp filter_contracts(basic_query, _), do: basic_query
|
|
|
|
def count_verified_contracts do
|
|
Repo.aggregate(SmartContract, :count, timeout: :infinity)
|
|
end
|
|
|
|
def count_new_verified_contracts do
|
|
query =
|
|
from(contract in SmartContract,
|
|
select: contract.inserted_at,
|
|
where: fragment("NOW() - ? at time zone 'UTC' <= interval '24 hours'", contract.inserted_at)
|
|
)
|
|
|
|
query
|
|
|> Repo.aggregate(:count, timeout: :infinity)
|
|
end
|
|
|
|
def count_contracts do
|
|
query =
|
|
from(address in Address,
|
|
select: address,
|
|
where: not is_nil(address.contract_code)
|
|
)
|
|
|
|
query
|
|
|> Repo.aggregate(:count, timeout: :infinity)
|
|
end
|
|
|
|
def count_new_contracts do
|
|
query =
|
|
from(tx in Transaction,
|
|
select: tx,
|
|
where:
|
|
tx.status == ^:ok and
|
|
fragment("NOW() - ? at time zone 'UTC' <= interval '24 hours'", tx.created_contract_code_indexed_at)
|
|
)
|
|
|
|
query
|
|
|> Repo.aggregate(:count, timeout: :infinity)
|
|
end
|
|
|
|
def count_verified_contracts_from_cache(options \\ []) do
|
|
VerifiedContractsCounter.fetch(options)
|
|
end
|
|
|
|
def count_new_verified_contracts_from_cache(options \\ []) do
|
|
NewVerifiedContractsCounter.fetch(options)
|
|
end
|
|
|
|
def count_contracts_from_cache(options \\ []) do
|
|
ContractsCounter.fetch(options)
|
|
end
|
|
|
|
def count_new_contracts_from_cache(options \\ []) do
|
|
NewContractsCounter.fetch(options)
|
|
end
|
|
|
|
def fetch_token_counters(address_hash, timeout) do
|
|
total_token_transfers_task =
|
|
Task.async(fn ->
|
|
TokenTransfersCounter.fetch(address_hash)
|
|
end)
|
|
|
|
total_token_holders_task =
|
|
Task.async(fn ->
|
|
TokenHoldersCounter.fetch(address_hash)
|
|
end)
|
|
|
|
[total_token_transfers_task, total_token_holders_task]
|
|
|> Task.yield_many(timeout)
|
|
|> Enum.map(fn {_task, res} ->
|
|
case res do
|
|
{:ok, result} ->
|
|
result
|
|
|
|
{:exit, reason} ->
|
|
Logger.warn("Query fetching token counters terminated: #{inspect(reason)}")
|
|
0
|
|
|
|
nil ->
|
|
Logger.warn("Query fetching token counters timed out.")
|
|
0
|
|
end
|
|
end)
|
|
|> List.to_tuple()
|
|
end
|
|
|
|
@spec flat_1155_batch_token_transfers([TokenTransfer.t()], Decimal.t() | nil) :: [TokenTransfer.t()]
|
|
def flat_1155_batch_token_transfers(token_transfers, token_id \\ nil) when is_list(token_transfers) do
|
|
Enum.reduce(token_transfers, [], fn tt, acc ->
|
|
case tt.token_ids do
|
|
[] ->
|
|
Enum.reverse([tt | Enum.reverse(acc)])
|
|
|
|
[_token_id] ->
|
|
Enum.reverse([tt | Enum.reverse(acc)])
|
|
|
|
token_ids when is_list(token_ids) ->
|
|
transfers = flat_1155_batch_token_transfer(tt, tt.amounts, token_ids, token_id)
|
|
|
|
acc ++ transfers
|
|
|
|
_ ->
|
|
Enum.reverse([tt | Enum.reverse(acc)])
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp flat_1155_batch_token_transfer(tt, amounts, token_ids, token_id_to_filter) do
|
|
amounts
|
|
|> Enum.zip(token_ids)
|
|
|> Enum.with_index()
|
|
|> Enum.map(fn {{amount, token_id}, index} ->
|
|
if is_nil(token_id_to_filter) || token_id == token_id_to_filter do
|
|
%TokenTransfer{tt | token_ids: [token_id], amount: amount, amounts: nil, index_in_batch: index}
|
|
end
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> squash_token_transfers_in_batch()
|
|
end
|
|
|
|
defp squash_token_transfers_in_batch(token_transfers) do
|
|
token_transfers
|
|
|> Enum.group_by(fn tt -> {List.first(tt.token_ids), tt.from_address_hash, tt.to_address_hash} end)
|
|
|> Enum.map(fn {_k, v} -> Enum.reduce(v, nil, &group_batch_reducer/2) end)
|
|
|> Enum.sort_by(fn tt -> tt.index_in_batch end, :desc)
|
|
end
|
|
|
|
defp group_batch_reducer(transfer, nil) do
|
|
transfer
|
|
end
|
|
|
|
defp group_batch_reducer(transfer, acc) do
|
|
%TokenTransfer{acc | amount: Decimal.add(acc.amount, transfer.amount)}
|
|
end
|
|
|
|
@spec paginate_1155_batch_token_transfers([TokenTransfer.t()], [paging_options]) :: [TokenTransfer.t()]
|
|
def paginate_1155_batch_token_transfers(token_transfers, options) do
|
|
paging_options = options |> Keyword.get(:paging_options, nil)
|
|
|
|
case paging_options do
|
|
%PagingOptions{batch_key: batch_key} when not is_nil(batch_key) ->
|
|
filter_previous_page_transfers(token_transfers, batch_key)
|
|
|
|
_ ->
|
|
token_transfers
|
|
end
|
|
end
|
|
|
|
defp filter_previous_page_transfers(
|
|
token_transfers,
|
|
{batch_block_hash, batch_transaction_hash, batch_log_index, index_in_batch}
|
|
) do
|
|
token_transfers
|
|
|> Enum.reverse()
|
|
|> Enum.reduce_while([], fn tt, acc ->
|
|
if tt.block_hash == batch_block_hash and tt.transaction_hash == batch_transaction_hash and
|
|
tt.log_index == batch_log_index and tt.index_in_batch == index_in_batch do
|
|
{:halt, acc}
|
|
else
|
|
{:cont, [tt | acc]}
|
|
end
|
|
end)
|
|
end
|
|
|
|
def select_repo(options) do
|
|
if Keyword.get(options, :api?, false) do
|
|
Repo.replica()
|
|
else
|
|
Repo
|
|
end
|
|
end
|
|
|
|
def select_watchlist_address_id(watchlist_id, address_hash)
|
|
when not is_nil(watchlist_id) and not is_nil(address_hash) do
|
|
WatchlistAddress
|
|
|> where([wa], wa.watchlist_id == ^watchlist_id and wa.address_hash_hash == ^address_hash)
|
|
|> select([wa], wa.id)
|
|
|> Repo.account_repo().one()
|
|
end
|
|
|
|
def select_watchlist_address_id(_watchlist_id, _address_hash), do: nil
|
|
|
|
def fetch_watchlist_transactions(watchlist_id, options) do
|
|
watchlist_addresses =
|
|
watchlist_id
|
|
|> WatchlistAddress.watchlist_addresses_by_watchlist_id_query()
|
|
|> Repo.account_repo().all()
|
|
|
|
address_hashes = Enum.map(watchlist_addresses, fn wa -> wa.address_hash end)
|
|
|
|
watchlist_names =
|
|
Enum.reduce(watchlist_addresses, %{}, fn wa, acc ->
|
|
Map.put(acc, wa.address_hash, %{label: wa.name, display_name: wa.name})
|
|
end)
|
|
|
|
{watchlist_names, address_hashes_to_mined_transactions_without_rewards(address_hashes, options)}
|
|
end
|
|
|
|
def list_withdrawals(options \\ []) do
|
|
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
|
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
|
|
|
|
Withdrawal.list_withdrawals()
|
|
|> join_associations(necessity_by_association)
|
|
|> handle_withdrawals_paging_options(paging_options)
|
|
|> select_repo(options).all()
|
|
end
|
|
|
|
def sum_withdrawals do
|
|
Repo.aggregate(Withdrawal, :sum, :amount, timeout: :infinity)
|
|
end
|
|
|
|
def upsert_count_withdrawals(index) do
|
|
upsert_last_fetched_counter(%{
|
|
counter_type: "withdrawals_count",
|
|
value: index
|
|
})
|
|
end
|
|
|
|
def sum_withdrawals_from_cache(options \\ []) do
|
|
WithdrawalsSum.fetch(options)
|
|
end
|
|
|
|
def count_withdrawals_from_cache(options \\ []) do
|
|
"withdrawals_count" |> get_last_fetched_counter(options) |> Decimal.add(1)
|
|
end
|
|
|
|
def add_fetcher_limit(query, false), do: query
|
|
|
|
def add_fetcher_limit(query, true) do
|
|
fetcher_limit = Application.get_env(:indexer, :fetcher_init_limit)
|
|
|
|
limit(query, ^fetcher_limit)
|
|
end
|
|
|
|
defp add_token_balances_fetcher_limit(query, false), do: query
|
|
|
|
defp add_token_balances_fetcher_limit(query, true) do
|
|
token_balances_fetcher_limit = Application.get_env(:indexer, :token_balances_fetcher_init_limit)
|
|
|
|
limit(query, ^token_balances_fetcher_limit)
|
|
end
|
|
|
|
def put_has_token_transfers_to_tx(query, true), do: query
|
|
|
|
def put_has_token_transfers_to_tx(query, false) do
|
|
from(tx in query,
|
|
select_merge: %{
|
|
has_token_transfers:
|
|
fragment(
|
|
"(SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NOT NULL",
|
|
tx.hash
|
|
)
|
|
}
|
|
)
|
|
end
|
|
|
|
@spec verified_contracts_top(non_neg_integer()) :: [Hash.Address.t()]
|
|
def verified_contracts_top(limit) do
|
|
query =
|
|
from(contract in SmartContract,
|
|
inner_join: address in Address,
|
|
on: contract.address_hash == address.hash,
|
|
order_by: [desc: address.transactions_count],
|
|
limit: ^limit,
|
|
select: contract.address_hash
|
|
)
|
|
|
|
Repo.all(query)
|
|
end
|
|
end
|
|
|