API v2 for frontend (#6379)
* Core BlockScout API V2 (#6164) API endpoints: /transactions /transactions/{tx_hash} /transactions/{tx_hash}/token-transfers /transactions/{tx_hash}/internal-transactions /transactions/{tx_hash}/logs /transactions/{tx_hash}/raw-trace * Block initial commit * Finish Block API * Address API initial commit * Fix some issue with API * Add tags * transaction -> coin_transfer * Some changes * Add /transactions and /token-transfers for addresses * Fix test * Fix tests * Fix block rewards * Add /json-rpc-url API endpoint * Create method_id index concurrently * Fix for concurrent index creation * BS core API V2: addresses, stats, main page, websockets (#6361) * socket/v2 * Refactor: reuse fees counter fucntion, remove unecessary clause of do_token_transfer_amount_for_api * Improve token transfers preload * Done with channels * Add some endpoints to /address * Fix credo * Add main page controller * Add stats controller * Move api search to API v2 * Fix some addresses methods; Rename gas_price * Improve logs view * Fix tests * Brush and finalize websockets * Add API_V2_ENABLED env * Fix credo * Add is_smart_contract clause * Add CHANGELOG entry Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> Co-authored-by: Никита Поздняков <nikitosing4@mail.ru>pull/5944/head
parent
2c438e6077
commit
3cac89f15c
@ -0,0 +1,19 @@ |
|||||||
|
defmodule BlockScoutWeb.UserSocketV2 do |
||||||
|
@moduledoc """ |
||||||
|
Module to distinct new and old UI websocket connections |
||||||
|
""" |
||||||
|
use Phoenix.Socket |
||||||
|
|
||||||
|
channel("addresses:*", BlockScoutWeb.AddressChannel) |
||||||
|
channel("blocks:*", BlockScoutWeb.BlockChannel) |
||||||
|
channel("exchange_rate:*", BlockScoutWeb.ExchangeRateChannel) |
||||||
|
channel("rewards:*", BlockScoutWeb.RewardChannel) |
||||||
|
channel("transactions:*", BlockScoutWeb.TransactionChannel) |
||||||
|
channel("tokens:*", BlockScoutWeb.TokenChannel) |
||||||
|
|
||||||
|
def connect(_params, socket) do |
||||||
|
{:ok, socket} |
||||||
|
end |
||||||
|
|
||||||
|
def id(_socket), do: nil |
||||||
|
end |
@ -0,0 +1,238 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.AddressController do |
||||||
|
use BlockScoutWeb, :controller |
||||||
|
|
||||||
|
import BlockScoutWeb.Chain, |
||||||
|
only: [ |
||||||
|
next_page_params: 3, |
||||||
|
paging_options: 1, |
||||||
|
split_list_by_page: 1, |
||||||
|
current_filter: 1 |
||||||
|
] |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView} |
||||||
|
alias Explorer.{Chain, Market} |
||||||
|
alias Indexer.Fetcher.TokenBalanceOnDemand |
||||||
|
|
||||||
|
@transaction_necessity_by_association [ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
:block => :optional, |
||||||
|
[created_contract_address: :smart_contract] => :optional, |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
@transaction_with_tt_necessity_by_association [ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[created_contract_address: :smart_contract] => :optional, |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional, |
||||||
|
[token_transfers: :token] => :optional, |
||||||
|
[token_transfers: :to_address] => :optional, |
||||||
|
[token_transfers: :from_address] => :optional, |
||||||
|
[token_transfers: :token_contract_address] => :optional, |
||||||
|
:block => :required |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
action_fallback(BlockScoutWeb.API.V2.FallbackController) |
||||||
|
|
||||||
|
def address(conn, %{"address_hash" => address_hash_string}) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, |
||||||
|
{:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash)} do |
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:address, %{address: address}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def token_balances(conn, %{"address_hash" => address_hash_string}) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
token_balances = |
||||||
|
address_hash |
||||||
|
|> Chain.fetch_last_token_balances() |
||||||
|
|
||||||
|
Task.start_link(fn -> |
||||||
|
TokenBalanceOnDemand.trigger_fetch(address_hash, token_balances) |
||||||
|
end) |
||||||
|
|
||||||
|
token_balances_with_price = |
||||||
|
token_balances |
||||||
|
|> Market.add_price() |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:token_balances, %{token_balances: token_balances_with_price}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def transactions(conn, %{"address_hash" => address_hash_string} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
options = |
||||||
|
@transaction_necessity_by_association |
||||||
|
|> Keyword.merge(paging_options(params)) |
||||||
|
|> Keyword.merge(current_filter(params)) |
||||||
|
|
||||||
|
results_plus_one = Chain.address_to_transactions_with_rewards(address_hash, options) |
||||||
|
{transactions, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, transactions, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def token_transfers(conn, %{"address_hash" => address_hash_string} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
options = |
||||||
|
@transaction_with_tt_necessity_by_association |
||||||
|
|> Keyword.merge(paging_options(params)) |
||||||
|
|> Keyword.merge(current_filter(params)) |
||||||
|
|
||||||
|
results_plus_one = |
||||||
|
Chain.address_hash_to_token_transfers( |
||||||
|
address_hash, |
||||||
|
options |
||||||
|
) |
||||||
|
|
||||||
|
{transactions, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, transactions, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def internal_transactions(conn, %{"address_hash" => address_hash_string} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
full_options = |
||||||
|
[ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[created_contract_address: :smart_contract] => :optional, |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional |
||||||
|
} |
||||||
|
] |
||||||
|
|> Keyword.merge(paging_options(params)) |
||||||
|
|> Keyword.merge(current_filter(params)) |
||||||
|
|
||||||
|
results_plus_one = Chain.address_to_internal_transactions(address_hash, full_options) |
||||||
|
{internal_transactions, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, internal_transactions, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:internal_transactions, %{ |
||||||
|
internal_transactions: internal_transactions, |
||||||
|
next_page_params: next_page_params |
||||||
|
}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def logs(conn, %{"address_hash" => address_hash_string, "topic" => topic} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
prepared_topic = String.trim(topic) |
||||||
|
|
||||||
|
formatted_topic = if String.starts_with?(prepared_topic, "0x"), do: prepared_topic, else: "0x" <> prepared_topic |
||||||
|
|
||||||
|
results_plus_one = Chain.address_to_logs(address_hash, topic: formatted_topic) |
||||||
|
|
||||||
|
{logs, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, logs, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:logs, %{logs: logs, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def logs(conn, %{"address_hash" => address_hash_string} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
results_plus_one = Chain.address_to_logs(address_hash, paging_options(params)) |
||||||
|
{logs, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, logs, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:logs, %{logs: logs, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def blocks_validated(conn, %{"address_hash" => address_hash_string} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
full_options = |
||||||
|
Keyword.merge( |
||||||
|
[ |
||||||
|
necessity_by_association: %{ |
||||||
|
miner: :required, |
||||||
|
nephews: :optional, |
||||||
|
transactions: :optional, |
||||||
|
rewards: :optional |
||||||
|
} |
||||||
|
], |
||||||
|
paging_options(params) |
||||||
|
) |
||||||
|
|
||||||
|
results_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash) |
||||||
|
{blocks, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, blocks, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(BlockView) |
||||||
|
|> render(:blocks, %{blocks: blocks, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def coin_balance_history(conn, %{"address_hash" => address_hash_string} = params) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
full_options = paging_options(params) |
||||||
|
|
||||||
|
results_plus_one = Chain.address_to_coin_balances(address_hash, full_options) |
||||||
|
|
||||||
|
{coin_balances, next_page} = split_list_by_page(results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, coin_balances, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(AddressView) |
||||||
|
|> render(:coin_balances, %{coin_balances: coin_balances, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def coin_balance_history_by_day(conn, %{"address_hash" => address_hash_string}) do |
||||||
|
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do |
||||||
|
balances_by_day = |
||||||
|
address_hash |
||||||
|
|> Chain.address_to_balances_by_day(true) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(AddressView) |
||||||
|
|> render(:coin_balances_by_day, %{coin_balances_by_day: balances_by_day}) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,81 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.BlockController do |
||||||
|
use BlockScoutWeb, :controller |
||||||
|
|
||||||
|
import BlockScoutWeb.Chain, |
||||||
|
only: [next_page_params: 3, paging_options: 1, put_key_value_to_paging_options: 3, split_list_by_page: 1] |
||||||
|
|
||||||
|
import BlockScoutWeb.PagingHelper, only: [select_block_type: 1] |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2.TransactionView |
||||||
|
alias BlockScoutWeb.BlockTransactionController |
||||||
|
alias Explorer.Chain |
||||||
|
|
||||||
|
@transaction_necessity_by_association [ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
:block => :optional, |
||||||
|
[created_contract_address: :smart_contract] => :optional, |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
action_fallback(BlockScoutWeb.API.V2.FallbackController) |
||||||
|
|
||||||
|
def block(conn, %{"block_hash_or_number" => block_hash_or_number}) do |
||||||
|
with {:ok, block} <- |
||||||
|
BlockTransactionController.param_block_hash_or_number_to_block(block_hash_or_number, |
||||||
|
necessity_by_association: %{ |
||||||
|
[miner: :names] => :required, |
||||||
|
:uncles => :optional, |
||||||
|
:nephews => :optional, |
||||||
|
:rewards => :optional, |
||||||
|
:transactions => :optional |
||||||
|
} |
||||||
|
) do |
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:block, %{block: block}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def blocks(conn, params) do |
||||||
|
full_options = select_block_type(params) |
||||||
|
|
||||||
|
blocks_plus_one = |
||||||
|
full_options |
||||||
|
|> Keyword.merge(paging_options(params)) |
||||||
|
|> Chain.list_blocks() |
||||||
|
|
||||||
|
{blocks, next_page} = split_list_by_page(blocks_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, blocks, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:blocks, %{blocks: blocks, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
|
||||||
|
def transactions(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do |
||||||
|
with {:ok, block} <- BlockTransactionController.param_block_hash_or_number_to_block(block_hash_or_number, []) do |
||||||
|
full_options = |
||||||
|
Keyword.merge( |
||||||
|
@transaction_necessity_by_association, |
||||||
|
put_key_value_to_paging_options(paging_options(params), :is_index_in_asc_order, true) |
||||||
|
) |
||||||
|
|
||||||
|
transactions_plus_one = Chain.block_to_transactions(block.hash, full_options, false) |
||||||
|
|
||||||
|
{transactions, next_page} = split_list_by_page(transactions_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, transactions, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,11 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.ConfigController do |
||||||
|
use BlockScoutWeb, :controller |
||||||
|
|
||||||
|
def json_rpc_url(conn, _params) do |
||||||
|
json_rpc_url = Application.get_env(:block_scout_web, :json_rpc) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:json_rpc_url, %{url: json_rpc_url}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,38 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.FallbackController do |
||||||
|
use Phoenix.Controller |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2.ApiView |
||||||
|
|
||||||
|
def call(conn, {:format, _}) do |
||||||
|
conn |
||||||
|
|> put_status(:unprocessable_entity) |
||||||
|
|> put_view(ApiView) |
||||||
|
|> render(:message, %{message: "Invalid parameter(s)"}) |
||||||
|
end |
||||||
|
|
||||||
|
def call(conn, {:not_found, _}) do |
||||||
|
conn |
||||||
|
|> put_status(:not_found) |
||||||
|
|> put_view(ApiView) |
||||||
|
|> render(:message, %{message: "Not found"}) |
||||||
|
end |
||||||
|
|
||||||
|
def call(conn, {:error, {:invalid, :hash}}) do |
||||||
|
conn |
||||||
|
|> put_status(:unprocessable_entity) |
||||||
|
|> put_view(ApiView) |
||||||
|
|> render(:message, %{message: "Invalid hash"}) |
||||||
|
end |
||||||
|
|
||||||
|
def call(conn, {:error, {:invalid, :number}}) do |
||||||
|
conn |
||||||
|
|> put_status(:unprocessable_entity) |
||||||
|
|> put_view(ApiView) |
||||||
|
|> render(:message, %{message: "Invalid number"}) |
||||||
|
end |
||||||
|
|
||||||
|
def call(conn, {:error, :not_found}) do |
||||||
|
conn |
||||||
|
|> call({:not_found, nil}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,40 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.MainPageController do |
||||||
|
use Phoenix.Controller |
||||||
|
|
||||||
|
alias Explorer.{Chain, PagingOptions} |
||||||
|
alias BlockScoutWeb.API.V2.{BlockView, TransactionView} |
||||||
|
alias Explorer.{Chain, Repo} |
||||||
|
|
||||||
|
def blocks(conn, _params) do |
||||||
|
blocks = |
||||||
|
[paging_options: %PagingOptions{page_size: 4}] |
||||||
|
|> Chain.list_blocks() |
||||||
|
|> Repo.preload([[miner: :names], :transactions, :rewards]) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(BlockView) |
||||||
|
|> render(:blocks, %{blocks: blocks}) |
||||||
|
end |
||||||
|
|
||||||
|
def transactions(conn, _params) do |
||||||
|
recent_transactions = |
||||||
|
Chain.recent_collated_transactions(false, |
||||||
|
necessity_by_association: %{ |
||||||
|
:block => :required, |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[created_contract_address: :smart_contract] => :optional, |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional |
||||||
|
}, |
||||||
|
paging_options: %PagingOptions{page_size: 5} |
||||||
|
) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_view(TransactionView) |
||||||
|
|> render(:transactions, %{transactions: recent_transactions}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,24 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.SearchController do |
||||||
|
use Phoenix.Controller |
||||||
|
|
||||||
|
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
|
||||||
|
def search(conn, %{"q" => query} = params) do |
||||||
|
[paging_options: paging_options] = paging_options(params) |
||||||
|
offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size |
||||||
|
|
||||||
|
search_results_plus_one = |
||||||
|
paging_options |
||||||
|
|> Chain.joint_search(offset, query) |
||||||
|
|
||||||
|
{search_results, next_page} = split_list_by_page(search_results_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, search_results, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:search_results, %{search_results: search_results, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,104 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.StatsController do |
||||||
|
use Phoenix.Controller |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2.Helper |
||||||
|
alias Explorer.{Chain, Market} |
||||||
|
alias Explorer.Chain.Cache.Block, as: BlockCache |
||||||
|
alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage} |
||||||
|
alias Explorer.Chain.Cache.Transaction, as: TransactionCache |
||||||
|
alias Explorer.Chain.Supply.RSK |
||||||
|
alias Explorer.Chain.Transaction.History.TransactionStats |
||||||
|
alias Explorer.Counters.AverageBlockTime |
||||||
|
alias Explorer.ExchangeRates.Token |
||||||
|
alias Timex.Duration |
||||||
|
|
||||||
|
def stats(conn, _params) do |
||||||
|
market_cap_type = |
||||||
|
case Application.get_env(:explorer, :supply) do |
||||||
|
RSK -> |
||||||
|
RSK |
||||||
|
|
||||||
|
_ -> |
||||||
|
:standard |
||||||
|
end |
||||||
|
|
||||||
|
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() |
||||||
|
|
||||||
|
transaction_stats = Helper.get_transaction_stats() |
||||||
|
|
||||||
|
gas_prices = |
||||||
|
case GasPriceOracle.get_gas_prices() do |
||||||
|
{:ok, gas_prices} -> |
||||||
|
gas_prices |
||||||
|
|
||||||
|
_ -> |
||||||
|
nil |
||||||
|
end |
||||||
|
|
||||||
|
gas_price = Application.get_env(:block_scout_web, :gas_price) |
||||||
|
|
||||||
|
json( |
||||||
|
conn, |
||||||
|
%{ |
||||||
|
"total_blocks" => BlockCache.estimated_count() |> to_string(), |
||||||
|
"total_addresses" => Chain.address_estimated_count() |> to_string(), |
||||||
|
"total_transactions" => TransactionCache.estimated_count() |> to_string(), |
||||||
|
"average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(), |
||||||
|
"coin_price" => exchange_rate.usd_value, |
||||||
|
"total_gas_used" => GasUsage.total() |> to_string(), |
||||||
|
"transactions_today" => Enum.at(transaction_stats, 0).number_of_transactions |> to_string(), |
||||||
|
"gas_used_today" => Enum.at(transaction_stats, 0).gas_used, |
||||||
|
"gas_prices" => gas_prices, |
||||||
|
"static_gas_price" => gas_price, |
||||||
|
"market_cap" => Helper.market_cap(market_cap_type, exchange_rate) |
||||||
|
} |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
def transactions_chart(conn, _params) do |
||||||
|
[{:history_size, history_size}] = |
||||||
|
Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}]) |
||||||
|
|
||||||
|
today = Date.utc_today() |
||||||
|
latest = Date.add(today, -1) |
||||||
|
earliest = Date.add(latest, -1 * history_size) |
||||||
|
|
||||||
|
date_range = TransactionStats.by_date_range(earliest, latest) |
||||||
|
|
||||||
|
transaction_history_data = |
||||||
|
date_range |
||||||
|
|> Enum.map(fn row -> |
||||||
|
%{date: row.date, tx_count: row.number_of_transactions} |
||||||
|
end) |
||||||
|
|
||||||
|
json(conn, %{ |
||||||
|
chart_data: transaction_history_data |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
def market_chart(conn, _params) do |
||||||
|
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() |
||||||
|
|
||||||
|
recent_market_history = Market.fetch_recent_history() |
||||||
|
|
||||||
|
market_history_data = |
||||||
|
recent_market_history |
||||||
|
|> case do |
||||||
|
[today | the_rest] -> |
||||||
|
[%{today | closing_price: exchange_rate.usd_value} | the_rest] |
||||||
|
|
||||||
|
data -> |
||||||
|
data |
||||||
|
end |
||||||
|
|> Enum.map(fn day -> Map.take(day, [:closing_price, :date]) end) |
||||||
|
|
||||||
|
json(conn, %{ |
||||||
|
chart_data: market_history_data, |
||||||
|
available_supply: available_supply(Chain.supply_for_days(), exchange_rate) |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
defp available_supply(:ok, exchange_rate), do: exchange_rate.available_supply || 0 |
||||||
|
|
||||||
|
defp available_supply({:ok, supply_for_days}, _exchange_rate), do: supply_for_days |
||||||
|
end |
@ -0,0 +1,221 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.TransactionController do |
||||||
|
use BlockScoutWeb, :controller |
||||||
|
|
||||||
|
import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1] |
||||||
|
|
||||||
|
import BlockScoutWeb.PagingHelper, |
||||||
|
only: [paging_options: 2, filter_options: 1, method_filter_options: 1, type_filter_options: 1] |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.Chain.Import |
||||||
|
alias Explorer.Chain.Import.Runner.InternalTransactions |
||||||
|
|
||||||
|
action_fallback(BlockScoutWeb.API.V2.FallbackController) |
||||||
|
|
||||||
|
@transaction_necessity_by_association %{ |
||||||
|
:block => :optional, |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[created_contract_address: :token] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional |
||||||
|
} |
||||||
|
|
||||||
|
@token_transfers_neccessity_by_association %{ |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
from_address: :required, |
||||||
|
to_address: :required, |
||||||
|
token: :required |
||||||
|
} |
||||||
|
|
||||||
|
@internal_transaction_neccessity_by_association [ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[transaction: :block] => :optional, |
||||||
|
[created_contract_address: :smart_contract] => :optional, |
||||||
|
[from_address: :smart_contract] => :optional, |
||||||
|
[to_address: :smart_contract] => :optional |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
def transaction(conn, %{"transaction_hash" => transaction_hash_string}) do |
||||||
|
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, |
||||||
|
{:not_found, {:ok, transaction}} <- |
||||||
|
{:not_found, |
||||||
|
Chain.hash_to_transaction( |
||||||
|
transaction_hash, |
||||||
|
necessity_by_association: @transaction_necessity_by_association |
||||||
|
)}, |
||||||
|
preloaded <- Chain.preload_token_transfers(transaction, @token_transfers_neccessity_by_association, false) do |
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:transaction, %{transaction: preloaded}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def transactions(conn, params) do |
||||||
|
filter_options = filter_options(params) |
||||||
|
method_filter_options = method_filter_options(params) |
||||||
|
type_filter_options = type_filter_options(params) |
||||||
|
|
||||||
|
full_options = |
||||||
|
Keyword.merge( |
||||||
|
[ |
||||||
|
necessity_by_association: @transaction_necessity_by_association |
||||||
|
], |
||||||
|
paging_options(params, filter_options) |
||||||
|
) |
||||||
|
|
||||||
|
transactions_plus_one = |
||||||
|
Chain.recent_transactions(full_options, filter_options, method_filter_options, type_filter_options) |
||||||
|
|
||||||
|
{transactions, next_page} = split_list_by_page(transactions_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, transactions, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
|
||||||
|
def raw_trace(conn, %{"transaction_hash" => transaction_hash_string}) do |
||||||
|
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, |
||||||
|
{:not_found, {:ok, transaction}} <- |
||||||
|
{:not_found, Chain.hash_to_transaction(transaction_hash)} do |
||||||
|
if is_nil(transaction.block_number) do |
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:raw_trace, %{internal_transactions: []}) |
||||||
|
else |
||||||
|
internal_transactions = Chain.all_transaction_to_internal_transactions(transaction_hash) |
||||||
|
|
||||||
|
first_trace_exists = |
||||||
|
Enum.find_index(internal_transactions, fn trace -> |
||||||
|
trace.index == 0 |
||||||
|
end) |
||||||
|
|
||||||
|
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) |
||||||
|
|
||||||
|
internal_transactions = |
||||||
|
if first_trace_exists do |
||||||
|
internal_transactions |
||||||
|
else |
||||||
|
response = |
||||||
|
Chain.fetch_first_trace( |
||||||
|
[ |
||||||
|
%{ |
||||||
|
block_hash: transaction.block_hash, |
||||||
|
block_number: transaction.block_number, |
||||||
|
hash_data: transaction_hash_string, |
||||||
|
transaction_index: transaction.index |
||||||
|
} |
||||||
|
], |
||||||
|
json_rpc_named_arguments |
||||||
|
) |
||||||
|
|
||||||
|
case response do |
||||||
|
{:ok, first_trace_params} -> |
||||||
|
InternalTransactions.run_insert_only(first_trace_params, %{ |
||||||
|
timeout: :infinity, |
||||||
|
timestamps: Import.timestamps(), |
||||||
|
internal_transactions: %{params: first_trace_params} |
||||||
|
}) |
||||||
|
|
||||||
|
Chain.all_transaction_to_internal_transactions(transaction_hash) |
||||||
|
|
||||||
|
{:error, _} -> |
||||||
|
internal_transactions |
||||||
|
|
||||||
|
:ignore -> |
||||||
|
internal_transactions |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:raw_trace, %{internal_transactions: internal_transactions}) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def token_transfers(conn, %{"transaction_hash" => transaction_hash_string} = params) do |
||||||
|
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)} do |
||||||
|
full_options = |
||||||
|
Keyword.merge( |
||||||
|
[ |
||||||
|
necessity_by_association: @token_transfers_neccessity_by_association |
||||||
|
], |
||||||
|
paging_options(params) |
||||||
|
) |
||||||
|
|
||||||
|
token_transfers_plus_one = Chain.transaction_to_token_transfers(transaction_hash, full_options) |
||||||
|
|
||||||
|
{token_transfers, next_page} = split_list_by_page(token_transfers_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, token_transfers, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def internal_transactions(conn, %{"transaction_hash" => transaction_hash_string} = params) do |
||||||
|
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)} do |
||||||
|
full_options = |
||||||
|
Keyword.merge( |
||||||
|
@internal_transaction_neccessity_by_association, |
||||||
|
paging_options(params) |
||||||
|
) |
||||||
|
|
||||||
|
internal_transactions_plus_one = Chain.transaction_to_internal_transactions(transaction_hash, full_options) |
||||||
|
|
||||||
|
{internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, internal_transactions, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:internal_transactions, %{ |
||||||
|
internal_transactions: internal_transactions, |
||||||
|
next_page_params: next_page_params |
||||||
|
}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def logs(conn, %{"transaction_hash" => transaction_hash_string} = params) do |
||||||
|
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)} do |
||||||
|
full_options = |
||||||
|
Keyword.merge( |
||||||
|
[ |
||||||
|
necessity_by_association: %{ |
||||||
|
[address: :names] => :optional, |
||||||
|
[address: :smart_contract] => :optional, |
||||||
|
address: :optional |
||||||
|
} |
||||||
|
], |
||||||
|
paging_options(params) |
||||||
|
) |
||||||
|
|
||||||
|
from_api = true |
||||||
|
logs_plus_one = Chain.transaction_to_logs(transaction_hash, from_api, full_options) |
||||||
|
|
||||||
|
{logs, next_page} = split_list_by_page(logs_plus_one) |
||||||
|
|
||||||
|
next_page_params = next_page_params(next_page, logs, params) |
||||||
|
|
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> render(:logs, %{ |
||||||
|
tx_hash: transaction_hash, |
||||||
|
logs: logs, |
||||||
|
next_page_params: next_page_params |
||||||
|
}) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -1,20 +0,0 @@ |
|||||||
defmodule BlockScoutWeb.PaginationHelpers do |
|
||||||
@moduledoc """ |
|
||||||
Common pagination logic helpers. |
|
||||||
""" |
|
||||||
|
|
||||||
def current_page_number(params) do |
|
||||||
cond do |
|
||||||
!params["prev_page_number"] -> 1 |
|
||||||
params["next_page"] -> String.to_integer(params["prev_page_number"]) + 1 |
|
||||||
params["prev_page"] -> String.to_integer(params["prev_page_number"]) - 1 |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
def add_navigation_params(params, current_page_path, current_page_number) do |
|
||||||
params |
|
||||||
|> Map.put("prev_page_path", current_page_path) |
|
||||||
|> Map.put("next_page", true) |
|
||||||
|> Map.put("prev_page_number", current_page_number) |
|
||||||
end |
|
||||||
end |
|
@ -0,0 +1,117 @@ |
|||||||
|
defmodule BlockScoutWeb.PagingHelper do |
||||||
|
@moduledoc """ |
||||||
|
Helper for fetching filters and other url query paramters |
||||||
|
""" |
||||||
|
import Explorer.Chain, only: [string_to_transaction_hash: 1] |
||||||
|
alias Explorer.PagingOptions |
||||||
|
|
||||||
|
@page_size 50 |
||||||
|
@default_paging_options %PagingOptions{page_size: @page_size + 1} |
||||||
|
@allowed_filter_labels ["validated", "pending"] |
||||||
|
@allowed_type_labels ["coin_transfer", "contract_call", "contract_creation", "token_transfer", "token_creation"] |
||||||
|
|
||||||
|
def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do |
||||||
|
with {block_number, ""} <- Integer.parse(block_number_string), |
||||||
|
{index, ""} <- Integer.parse(index_string) do |
||||||
|
[paging_options: %{@default_paging_options | key: {block_number, index}}] |
||||||
|
else |
||||||
|
_ -> |
||||||
|
[paging_options: @default_paging_options] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def paging_options(%{"inserted_at" => inserted_at_string, "hash" => hash_string}, [:pending | _]) do |
||||||
|
with {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string), |
||||||
|
{:ok, hash} <- string_to_transaction_hash(hash_string) do |
||||||
|
[paging_options: %{@default_paging_options | key: {inserted_at, hash}, is_pending_tx: true}] |
||||||
|
else |
||||||
|
_ -> |
||||||
|
[paging_options: @default_paging_options] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def paging_options(_params, _filter), do: [paging_options: @default_paging_options] |
||||||
|
|
||||||
|
def filter_options(%{"filter" => filter}) do |
||||||
|
parse_filter(filter, @allowed_filter_labels) |
||||||
|
end |
||||||
|
|
||||||
|
def filter_options(_params), do: [] |
||||||
|
|
||||||
|
def type_filter_options(%{"type" => type}) do |
||||||
|
parse_filter(type, @allowed_type_labels) |
||||||
|
end |
||||||
|
|
||||||
|
def type_filter_options(_params), do: [] |
||||||
|
|
||||||
|
def method_filter_options(%{"method" => method}) do |
||||||
|
parse_method_filter(method) |
||||||
|
end |
||||||
|
|
||||||
|
def method_filter_options(_params), do: [] |
||||||
|
|
||||||
|
def parse_filter("[" <> filter, allowed_labels) do |
||||||
|
filter |
||||||
|
|> String.trim_trailing("]") |
||||||
|
|> parse_filter(allowed_labels) |
||||||
|
end |
||||||
|
|
||||||
|
# sobelow_skip ["DOS.StringToAtom"] |
||||||
|
def parse_filter(filter, allowed_labels) when is_binary(filter) do |
||||||
|
filter |
||||||
|
|> String.split(",") |
||||||
|
|> Enum.filter(fn label -> Enum.member?(allowed_labels, label) end) |
||||||
|
|> Enum.uniq() |
||||||
|
|> Enum.map(&String.to_atom/1) |
||||||
|
end |
||||||
|
|
||||||
|
def parse_method_filter("[" <> filter) do |
||||||
|
filter |
||||||
|
|> String.trim_trailing("]") |
||||||
|
|> parse_method_filter() |
||||||
|
end |
||||||
|
|
||||||
|
def parse_method_filter(filter) do |
||||||
|
filter |
||||||
|
|> String.split(",") |
||||||
|
|> Enum.uniq() |
||||||
|
end |
||||||
|
|
||||||
|
def select_block_type(%{"type" => type}) do |
||||||
|
case String.downcase(type) do |
||||||
|
"uncle" -> |
||||||
|
[ |
||||||
|
necessity_by_association: %{ |
||||||
|
:transactions => :optional, |
||||||
|
[miner: :names] => :optional, |
||||||
|
:nephews => :required, |
||||||
|
:rewards => :optional |
||||||
|
}, |
||||||
|
block_type: "Uncle" |
||||||
|
] |
||||||
|
|
||||||
|
"reorg" -> |
||||||
|
[ |
||||||
|
necessity_by_association: %{ |
||||||
|
:transactions => :optional, |
||||||
|
[miner: :names] => :optional, |
||||||
|
:rewards => :optional |
||||||
|
}, |
||||||
|
block_type: "Reorg" |
||||||
|
] |
||||||
|
|
||||||
|
_ -> |
||||||
|
select_block_type(nil) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def select_block_type(_), |
||||||
|
do: [ |
||||||
|
necessity_by_association: %{ |
||||||
|
:transactions => :optional, |
||||||
|
[miner: :names] => :optional, |
||||||
|
:rewards => :optional |
||||||
|
}, |
||||||
|
block_type: "Block" |
||||||
|
] |
||||||
|
end |
@ -0,0 +1,21 @@ |
|||||||
|
defmodule BlockScoutWeb.Plug.CheckApiV2 do |
||||||
|
@moduledoc """ |
||||||
|
Checks if the API V2 enabled. |
||||||
|
""" |
||||||
|
import Plug.Conn |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2, as: API_V2 |
||||||
|
|
||||||
|
def init(opts), do: opts |
||||||
|
|
||||||
|
def call(conn, _opts) do |
||||||
|
if API_V2.enabled?() do |
||||||
|
conn |
||||||
|
else |
||||||
|
conn |
||||||
|
|> put_resp_content_type("application/json") |
||||||
|
|> send_resp(404, Jason.encode!(%{message: "API V2 is disabled"})) |
||||||
|
|> halt() |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,59 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.AddressView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} |
||||||
|
alias BlockScoutWeb.API.V2.Helper |
||||||
|
|
||||||
|
def render("message.json", assigns) do |
||||||
|
ApiView.render("message.json", assigns) |
||||||
|
end |
||||||
|
|
||||||
|
def render("address.json", %{address: address, conn: conn}) do |
||||||
|
prepare_address(address, conn) |
||||||
|
end |
||||||
|
|
||||||
|
def render("token_balances.json", %{token_balances: token_balances}) do |
||||||
|
Enum.map(token_balances, &prepare_token_balance/1) |
||||||
|
end |
||||||
|
|
||||||
|
def render("coin_balance.json", %{coin_balance: coin_balance}) do |
||||||
|
prepare_coin_balance_history_entry(coin_balance) |
||||||
|
end |
||||||
|
|
||||||
|
def render("coin_balances.json", %{coin_balances: coin_balances, next_page_params: next_page_params}) do |
||||||
|
%{"items" => Enum.map(coin_balances, &prepare_coin_balance_history_entry/1), "next_page_params" => next_page_params} |
||||||
|
end |
||||||
|
|
||||||
|
def render("coin_balances_by_day.json", %{coin_balances_by_day: coin_balances_by_day}) do |
||||||
|
Enum.map(coin_balances_by_day, &prepare_coin_balance_history_by_day_entry/1) |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_address(address, conn \\ nil) do |
||||||
|
Helper.address_with_info(conn, address, address.hash) |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_token_balance({token_balance, token}) do |
||||||
|
%{ |
||||||
|
"value" => token_balance.value, |
||||||
|
"token" => TokenView.render("token.json", %{token: token}), |
||||||
|
"token_id" => token_balance.token_id |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_coin_balance_history_entry(coin_balance) do |
||||||
|
%{ |
||||||
|
"transaction_hash" => coin_balance.transaction_hash, |
||||||
|
"block_number" => coin_balance.block_number, |
||||||
|
"delta" => coin_balance.delta, |
||||||
|
"value" => coin_balance.value, |
||||||
|
"block_timestamp" => coin_balance.block_timestamp |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_coin_balance_history_by_day_entry(coin_balance_by_day) do |
||||||
|
%{ |
||||||
|
"date" => coin_balance_by_day.date, |
||||||
|
"value" => coin_balance_by_day.value |
||||||
|
} |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,9 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2 do |
||||||
|
@moduledoc """ |
||||||
|
API V2 context |
||||||
|
""" |
||||||
|
|
||||||
|
def enabled? do |
||||||
|
Application.get_env(:block_scout_web, __MODULE__)[:enabled] |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.ApiView do |
||||||
|
def render("message.json", %{message: message}) do |
||||||
|
%{ |
||||||
|
"message" => message |
||||||
|
} |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,106 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.BlockView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
|
||||||
|
alias BlockScoutWeb.BlockView |
||||||
|
alias BlockScoutWeb.API.V2.{ApiView, Helper} |
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.Chain.Block |
||||||
|
alias Explorer.Counters.BlockPriorityFeeCounter |
||||||
|
|
||||||
|
def render("message.json", assigns) do |
||||||
|
ApiView.render("message.json", assigns) |
||||||
|
end |
||||||
|
|
||||||
|
def render("blocks.json", %{blocks: blocks, next_page_params: next_page_params}) do |
||||||
|
%{"items" => Enum.map(blocks, &prepare_block(&1, nil)), "next_page_params" => next_page_params} |
||||||
|
end |
||||||
|
|
||||||
|
def render("blocks.json", %{blocks: blocks}) do |
||||||
|
Enum.map(blocks, &prepare_block(&1, nil)) |
||||||
|
end |
||||||
|
|
||||||
|
def render("block.json", %{block: block, conn: conn}) do |
||||||
|
prepare_block(block, conn, true) |
||||||
|
end |
||||||
|
|
||||||
|
def render("block.json", %{block: block, socket: _socket}) do |
||||||
|
# single_block? set to true in order to prevent heavy fetching of reward type |
||||||
|
prepare_block(block, nil, false) |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_block(block, conn, single_block? \\ false) do |
||||||
|
burned_fee = Chain.burned_fees(block.transactions, block.base_fee_per_gas) |
||||||
|
priority_fee = block.base_fee_per_gas && BlockPriorityFeeCounter.fetch(block.hash) |
||||||
|
|
||||||
|
tx_fees = Chain.txn_fees(block.transactions) |
||||||
|
|
||||||
|
%{ |
||||||
|
"height" => block.number, |
||||||
|
"timestamp" => block.timestamp, |
||||||
|
"tx_count" => count_transactions(block), |
||||||
|
"miner" => Helper.address_with_info(conn, block.miner, block.miner_hash), |
||||||
|
"size" => block.size, |
||||||
|
"hash" => block.hash, |
||||||
|
"parent_hash" => block.parent_hash, |
||||||
|
"difficulty" => block.difficulty, |
||||||
|
"total_difficulty" => block.total_difficulty, |
||||||
|
"gas_used" => block.gas_used, |
||||||
|
"gas_limit" => block.gas_limit, |
||||||
|
"nonce" => block.nonce, |
||||||
|
"base_fee_per_gas" => block.base_fee_per_gas, |
||||||
|
"burnt_fees" => burned_fee, |
||||||
|
"priority_fee" => priority_fee, |
||||||
|
"extra_data" => "TODO", |
||||||
|
"uncles_hashes" => prepare_uncles(block.uncle_relations), |
||||||
|
"state_root" => "TODO", |
||||||
|
"rewards" => prepare_rewards(block.rewards, block, single_block?), |
||||||
|
"gas_target_percentage" => gas_target(block), |
||||||
|
"gas_used_percentage" => gas_used_percentage(block), |
||||||
|
"burnt_fees_percentage" => burnt_fees_percentage(burned_fee, tx_fees), |
||||||
|
"type" => block |> BlockView.block_type() |> String.downcase(), |
||||||
|
"tx_fees" => tx_fees |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_rewards(rewards, block, single_block?) do |
||||||
|
Enum.map(rewards, &prepare_reward(&1, block, single_block?)) |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_reward(reward, block, single_block?) do |
||||||
|
%{ |
||||||
|
"reward" => reward.reward, |
||||||
|
"type" => if(single_block?, do: BlockView.block_reward_text(reward, block.miner.hash), else: reward.address_type) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_uncles(uncles_relations) when is_list(uncles_relations) do |
||||||
|
Enum.map(uncles_relations, &prepare_uncle/1) |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_uncles(_), do: [] |
||||||
|
|
||||||
|
def prepare_uncle(uncle_relation) do |
||||||
|
%{"hash" => uncle_relation.uncle_hash} |
||||||
|
end |
||||||
|
|
||||||
|
def gas_target(block) do |
||||||
|
elasticity_multiplier = 2 |
||||||
|
ratio = Decimal.div(block.gas_used, Decimal.div(block.gas_limit, elasticity_multiplier)) |
||||||
|
ratio |> Decimal.sub(1) |> Decimal.mult(100) |> Decimal.to_float() |
||||||
|
end |
||||||
|
|
||||||
|
def gas_used_percentage(block) do |
||||||
|
block.gas_used |> Decimal.div(block.gas_limit) |> Decimal.mult(100) |> Decimal.to_float() |
||||||
|
end |
||||||
|
|
||||||
|
def burnt_fees_percentage(_, %Decimal{coef: 0}), do: nil |
||||||
|
|
||||||
|
def burnt_fees_percentage(burnt_fees, tx_fees) when not is_nil(tx_fees) and not is_nil(burnt_fees) do |
||||||
|
burnt_fees.value |> Decimal.div(tx_fees) |> Decimal.mult(100) |> Decimal.to_float() |
||||||
|
end |
||||||
|
|
||||||
|
def burnt_fees_percentage(_, _), do: nil |
||||||
|
|
||||||
|
def count_transactions(%Block{transactions: txs}) when is_list(txs), do: Enum.count(txs) |
||||||
|
def count_transactions(_), do: nil |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.ConfigView do |
||||||
|
def render("json_rpc_url.json", %{url: url}) do |
||||||
|
%{ |
||||||
|
"json_rpc_url" => url |
||||||
|
} |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,108 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.Helper do |
||||||
|
@moduledoc """ |
||||||
|
API V2 helper |
||||||
|
""" |
||||||
|
|
||||||
|
alias Ecto.Association.NotLoaded |
||||||
|
alias Explorer.Chain.Address |
||||||
|
alias Explorer.Chain.Transaction.History.TransactionStats |
||||||
|
|
||||||
|
import BlockScoutWeb.Account.AuthController, only: [current_user: 1] |
||||||
|
import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2, get_tags_on_address: 1] |
||||||
|
|
||||||
|
def address_with_info(conn, address, address_hash) do |
||||||
|
%{ |
||||||
|
personal_tags: private_tags, |
||||||
|
watchlist_names: watchlist_names |
||||||
|
} = get_address_tags(address_hash, current_user(conn)) |
||||||
|
|
||||||
|
public_tags = get_tags_on_address(address_hash) |
||||||
|
|
||||||
|
Map.merge(address_with_info(address, address_hash), %{ |
||||||
|
"private_tags" => private_tags, |
||||||
|
"watchlist_names" => watchlist_names, |
||||||
|
"public_tags" => public_tags |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
def address_with_info(%Address{} = address, _address_hash) do |
||||||
|
%{ |
||||||
|
"hash" => to_string(address), |
||||||
|
"is_contract" => is_smart_contract(address), |
||||||
|
"name" => address_name(address), |
||||||
|
"implementation_name" => implementation_name(address), |
||||||
|
"is_verified" => is_verified(address) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def address_with_info(%NotLoaded{}, address_hash) do |
||||||
|
address_with_info(nil, address_hash) |
||||||
|
end |
||||||
|
|
||||||
|
def address_with_info(nil, address_hash) do |
||||||
|
%{"hash" => address_hash, "is_contract" => false, "name" => nil, "implementation_name" => nil, "is_verified" => nil} |
||||||
|
end |
||||||
|
|
||||||
|
def address_name(%Address{names: [_ | _] = address_names}) do |
||||||
|
case Enum.find(address_names, &(&1.primary == true)) do |
||||||
|
nil -> |
||||||
|
%Address.Name{name: name} = Enum.at(address_names, 0) |
||||||
|
name |
||||||
|
|
||||||
|
%Address.Name{name: name} -> |
||||||
|
name |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def address_name(_), do: nil |
||||||
|
|
||||||
|
def implementation_name(%Address{smart_contract: %{implementation_name: implementation_name}}), |
||||||
|
do: implementation_name |
||||||
|
|
||||||
|
def implementation_name(_), do: nil |
||||||
|
|
||||||
|
def is_smart_contract(%Address{contract_code: nil}), do: false |
||||||
|
def is_smart_contract(%Address{contract_code: _}), do: true |
||||||
|
def is_smart_contract(%NotLoaded{}), do: nil |
||||||
|
def is_smart_contract(_), do: false |
||||||
|
|
||||||
|
def is_verified(%Address{smart_contract: nil}), do: false |
||||||
|
def is_verified(%Address{smart_contract: %NotLoaded{}}), do: nil |
||||||
|
def is_verified(%Address{smart_contract: _}), do: true |
||||||
|
|
||||||
|
def market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value}) |
||||||
|
when is_nil(available_supply) or is_nil(usd_value) do |
||||||
|
Decimal.new(0) |
||||||
|
end |
||||||
|
|
||||||
|
def market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value}) do |
||||||
|
Decimal.mult(available_supply, usd_value) |
||||||
|
end |
||||||
|
|
||||||
|
def market_cap(:standard, exchange_rate) do |
||||||
|
exchange_rate.market_cap_usd |
||||||
|
end |
||||||
|
|
||||||
|
def market_cap(module, exchange_rate) do |
||||||
|
module.market_cap(exchange_rate) |
||||||
|
end |
||||||
|
|
||||||
|
def get_transaction_stats do |
||||||
|
stats_scale = date_range(1) |
||||||
|
transaction_stats = TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest) |
||||||
|
|
||||||
|
# Need datapoint for legend if none currently available. |
||||||
|
if Enum.empty?(transaction_stats) do |
||||||
|
[%{number_of_transactions: 0, gas_used: 0}] |
||||||
|
else |
||||||
|
transaction_stats |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def date_range(num_days) do |
||||||
|
today = Date.utc_today() |
||||||
|
latest = Date.add(today, -1) |
||||||
|
x_days_back = Date.add(latest, -1 * (num_days - 1)) |
||||||
|
%{earliest: x_days_back, latest: latest} |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,53 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.SearchView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
|
||||||
|
alias BlockScoutWeb.Endpoint |
||||||
|
|
||||||
|
def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do |
||||||
|
%{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_search_result(%{type: "token"} = search_result) do |
||||||
|
%{ |
||||||
|
"type" => search_result.type, |
||||||
|
"name" => search_result.name, |
||||||
|
"symbol" => search_result.symbol, |
||||||
|
"address" => search_result.address_hash, |
||||||
|
"token_url" => token_path(Endpoint, :show, search_result.address_hash), |
||||||
|
"address_url" => address_path(Endpoint, :show, search_result.address_hash) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_search_result(%{type: address_or_contract} = search_result) |
||||||
|
when address_or_contract in ["address", "contract"] do |
||||||
|
%{ |
||||||
|
"type" => search_result.type, |
||||||
|
"name" => search_result.name, |
||||||
|
"address" => search_result.address_hash, |
||||||
|
"url" => address_path(Endpoint, :show, search_result.address_hash) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_search_result(%{type: "block"} = search_result) do |
||||||
|
block_hash = hash_to_string(search_result.block_hash) |
||||||
|
|
||||||
|
%{ |
||||||
|
"type" => search_result.type, |
||||||
|
"block_number" => search_result.block_number, |
||||||
|
"block_hash" => block_hash, |
||||||
|
"url" => block_path(Endpoint, :show, block_hash) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_search_result(%{type: "transaction"} = search_result) do |
||||||
|
tx_hash = hash_to_string(search_result.tx_hash) |
||||||
|
|
||||||
|
%{ |
||||||
|
"type" => search_result.type, |
||||||
|
"tx_hash" => tx_hash, |
||||||
|
"url" => transaction_path(Endpoint, :show, tx_hash) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower) |
||||||
|
end |
@ -0,0 +1,13 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.TokenView do |
||||||
|
def render("token.json", %{token: token}) do |
||||||
|
%{ |
||||||
|
"address" => token.contract_address_hash, |
||||||
|
"symbol" => token.symbol, |
||||||
|
"name" => token.name, |
||||||
|
"decimals" => token.decimals, |
||||||
|
"type" => token.type, |
||||||
|
"holders" => to_string(token.holder_count), |
||||||
|
"exchange_rate" => token.usd_value && to_string(token.usd_value) |
||||||
|
} |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,447 @@ |
|||||||
|
defmodule BlockScoutWeb.API.V2.TransactionView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
|
||||||
|
alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} |
||||||
|
alias BlockScoutWeb.{ABIEncodedValueView, TransactionView} |
||||||
|
alias BlockScoutWeb.Models.GetTransactionTags |
||||||
|
alias BlockScoutWeb.Tokens.Helpers |
||||||
|
alias Ecto.Association.NotLoaded |
||||||
|
alias Explorer.ExchangeRates.Token, as: TokenRate |
||||||
|
alias Explorer.{Chain, Market} |
||||||
|
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, Transaction, Wei} |
||||||
|
alias Explorer.Chain.Block.Reward |
||||||
|
alias Explorer.Counters.AverageBlockTime |
||||||
|
alias Timex.Duration |
||||||
|
|
||||||
|
import BlockScoutWeb.Account.AuthController, only: [current_user: 1] |
||||||
|
|
||||||
|
def render("message.json", assigns) do |
||||||
|
ApiView.render("message.json", assigns) |
||||||
|
end |
||||||
|
|
||||||
|
def render("transactions.json", %{transactions: transactions, conn: conn}) do |
||||||
|
Enum.map(transactions, &prepare_transaction(&1, conn, false)) |
||||||
|
end |
||||||
|
|
||||||
|
def render("transactions.json", %{transactions: transactions, next_page_params: next_page_params, conn: conn}) do |
||||||
|
%{"items" => Enum.map(transactions, &prepare_transaction(&1, conn, false)), "next_page_params" => next_page_params} |
||||||
|
end |
||||||
|
|
||||||
|
def render("transaction.json", %{transaction: transaction, conn: conn}) do |
||||||
|
prepare_transaction(transaction, conn, true) |
||||||
|
end |
||||||
|
|
||||||
|
def render("raw_trace.json", %{internal_transactions: internal_transactions}) do |
||||||
|
InternalTransaction.internal_transactions_to_raw(internal_transactions) |
||||||
|
end |
||||||
|
|
||||||
|
def render("decoded_log_input.json", %{method_id: method_id, text: text, mapping: mapping}) do |
||||||
|
%{"method_id" => method_id, "method_call" => text, "parameters" => prepare_log_mapping(mapping)} |
||||||
|
end |
||||||
|
|
||||||
|
def render("decoded_input.json", %{method_id: method_id, text: text, mapping: mapping, error?: _error}) do |
||||||
|
%{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)} |
||||||
|
end |
||||||
|
|
||||||
|
def render("revert_reason.json", %{raw: raw, decoded: decoded}) do |
||||||
|
%{"raw" => raw, "decoded" => decoded} |
||||||
|
end |
||||||
|
|
||||||
|
def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do |
||||||
|
%{"items" => Enum.map(token_transfers, &prepare_token_transfer(&1, conn)), "next_page_params" => next_page_params} |
||||||
|
end |
||||||
|
|
||||||
|
def render("token_transfers.json", %{token_transfers: token_transfers, conn: conn}) do |
||||||
|
Enum.map(token_transfers, &prepare_token_transfer(&1, conn)) |
||||||
|
end |
||||||
|
|
||||||
|
def render("token_transfer.json", %{token_transfer: token_transfer, conn: conn}) do |
||||||
|
prepare_token_transfer(token_transfer, conn) |
||||||
|
end |
||||||
|
|
||||||
|
def render("internal_transactions.json", %{ |
||||||
|
internal_transactions: internal_transactions, |
||||||
|
next_page_params: next_page_params, |
||||||
|
conn: conn |
||||||
|
}) do |
||||||
|
%{ |
||||||
|
"items" => Enum.map(internal_transactions, &prepare_internal_transaction(&1, conn)), |
||||||
|
"next_page_params" => next_page_params |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def render("logs.json", %{logs: logs, next_page_params: next_page_params, tx_hash: tx_hash}) do |
||||||
|
%{"items" => Enum.map(logs, fn log -> prepare_log(log, tx_hash) end), "next_page_params" => next_page_params} |
||||||
|
end |
||||||
|
|
||||||
|
def render("logs.json", %{logs: logs, next_page_params: next_page_params}) do |
||||||
|
%{ |
||||||
|
"items" => Enum.map(logs, fn log -> prepare_log(log, log.transaction) end), |
||||||
|
"next_page_params" => next_page_params |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_token_transfer(token_transfer, conn) do |
||||||
|
%{ |
||||||
|
"tx_hash" => token_transfer.transaction_hash, |
||||||
|
"from" => Helper.address_with_info(conn, token_transfer.from_address, token_transfer.from_address_hash), |
||||||
|
"to" => Helper.address_with_info(conn, token_transfer.to_address, token_transfer.to_address_hash), |
||||||
|
"total" => prepare_token_transfer_total(token_transfer), |
||||||
|
"token" => TokenView.render("token.json", %{token: Market.add_price(token_transfer.token)}), |
||||||
|
"type" => Chain.get_token_transfer_type(token_transfer) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_token_transfer_total(token_transfer) do |
||||||
|
case Helpers.token_transfer_amount_for_api(token_transfer) do |
||||||
|
{:ok, :erc721_instance} -> |
||||||
|
%{"token_id" => token_transfer.token_id} |
||||||
|
|
||||||
|
{:ok, :erc1155_instance, value, decimals} -> |
||||||
|
%{"token_id" => token_transfer.token_id, "value" => value, "decimals" => decimals} |
||||||
|
|
||||||
|
{:ok, :erc1155_instance, values, token_ids, decimals} -> |
||||||
|
Enum.map(Enum.zip(values, token_ids), fn {value, token_id} -> |
||||||
|
%{"value" => value, "token_id" => token_id, "decimals" => decimals} |
||||||
|
end) |
||||||
|
|
||||||
|
{:ok, value, decimals} -> |
||||||
|
%{"value" => value, "decimals" => decimals} |
||||||
|
|
||||||
|
_ -> |
||||||
|
nil |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_internal_transaction(internal_transaction, conn) do |
||||||
|
%{ |
||||||
|
"error" => internal_transaction.error, |
||||||
|
"success" => is_nil(internal_transaction.error), |
||||||
|
"type" => internal_transaction.call_type, |
||||||
|
"transaction_hash" => internal_transaction.transaction_hash, |
||||||
|
"from" => |
||||||
|
Helper.address_with_info( |
||||||
|
conn, |
||||||
|
internal_transaction.from_address, |
||||||
|
internal_transaction.from_address_hash |
||||||
|
), |
||||||
|
"to" => Helper.address_with_info(conn, internal_transaction.to_address, internal_transaction.to_address_hash), |
||||||
|
"created_contract" => |
||||||
|
Helper.address_with_info( |
||||||
|
conn, |
||||||
|
internal_transaction.created_contract_address, |
||||||
|
internal_transaction.created_contract_address_hash |
||||||
|
), |
||||||
|
"value" => internal_transaction.value, |
||||||
|
"block" => internal_transaction.block_number, |
||||||
|
"timestamp" => internal_transaction.transaction.block.timestamp, |
||||||
|
"index" => internal_transaction.index, |
||||||
|
"gas_limit" => internal_transaction.gas |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_log(log, transaction_or_hash) do |
||||||
|
decoded = decode_log(log, transaction_or_hash) |
||||||
|
|
||||||
|
%{ |
||||||
|
"address" => Helper.address_with_info(log.address, log.address_hash), |
||||||
|
"topics" => [ |
||||||
|
log.first_topic, |
||||||
|
log.second_topic, |
||||||
|
log.third_topic, |
||||||
|
log.fourth_topic |
||||||
|
], |
||||||
|
"data" => log.data, |
||||||
|
"index" => log.index, |
||||||
|
"decoded" => decoded, |
||||||
|
"smart_contract" => smart_contract_info(transaction_or_hash) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
defp smart_contract_info(%Transaction{} = tx), do: Helper.address_with_info(tx.to_address, tx.to_address_hash) |
||||||
|
defp smart_contract_info(_), do: nil |
||||||
|
|
||||||
|
defp decode_log(log, %Transaction{} = tx) do |
||||||
|
case log |> Log.decode(tx) |> format_decoded_log_input() do |
||||||
|
{:ok, method_id, text, mapping} -> |
||||||
|
render(__MODULE__, "decoded_log_input.json", method_id: method_id, text: text, mapping: mapping) |
||||||
|
|
||||||
|
_ -> |
||||||
|
nil |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp decode_log(log, transaction_hash), do: decode_log(log, %Transaction{hash: transaction_hash}) |
||||||
|
|
||||||
|
defp prepare_transaction({%Reward{} = emission_reward, %Reward{} = validator_reward}, conn, _single_tx?) do |
||||||
|
%{ |
||||||
|
"emission_reward" => emission_reward.reward, |
||||||
|
"block_hash" => validator_reward.block_hash, |
||||||
|
"from" => Helper.address_with_info(conn, emission_reward.address, emission_reward.address_hash), |
||||||
|
"to" => Helper.address_with_info(conn, validator_reward.address, validator_reward.address_hash), |
||||||
|
"types" => [:reward] |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
defp prepare_transaction(%Transaction{} = transaction, conn, single_tx?) do |
||||||
|
base_fee_per_gas = transaction.block && transaction.block.base_fee_per_gas |
||||||
|
max_priority_fee_per_gas = transaction.max_priority_fee_per_gas |
||||||
|
max_fee_per_gas = transaction.max_fee_per_gas |
||||||
|
|
||||||
|
priority_fee_per_gas = priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) |
||||||
|
|
||||||
|
burned_fee = burned_fee(transaction, max_fee_per_gas, base_fee_per_gas) |
||||||
|
|
||||||
|
status = transaction |> Chain.transaction_to_status() |> format_status() |
||||||
|
|
||||||
|
revert_reason = revert_reason(status, transaction) |
||||||
|
|
||||||
|
decoded_input = transaction |> Transaction.decoded_input_data() |> format_decoded_input() |
||||||
|
decoded_input_data = decoded_input(decoded_input) |
||||||
|
|
||||||
|
%{ |
||||||
|
"hash" => transaction.hash, |
||||||
|
"result" => status, |
||||||
|
"status" => transaction.status, |
||||||
|
"block" => transaction.block_number, |
||||||
|
"timestamp" => transaction.block && transaction.block.timestamp, |
||||||
|
"from" => Helper.address_with_info(conn, transaction.from_address, transaction.from_address_hash), |
||||||
|
"to" => Helper.address_with_info(conn, transaction.to_address, transaction.to_address_hash), |
||||||
|
"created_contract" => |
||||||
|
Helper.address_with_info(conn, transaction.created_contract_address, transaction.created_contract_address_hash), |
||||||
|
"confirmations" => |
||||||
|
transaction.block |> Chain.confirmations(block_height: Chain.block_height()) |> format_confirmations(), |
||||||
|
"confirmation_duration" => processing_time_duration(transaction), |
||||||
|
"value" => transaction.value, |
||||||
|
"fee" => transaction |> Chain.fee(:wei) |> format_fee(), |
||||||
|
"gas_price" => transaction.gas_price, |
||||||
|
"type" => transaction.type, |
||||||
|
"gas_used" => transaction.gas_used, |
||||||
|
"gas_limit" => transaction.gas, |
||||||
|
"max_fee_per_gas" => transaction.max_fee_per_gas, |
||||||
|
"max_priority_fee_per_gas" => transaction.max_priority_fee_per_gas, |
||||||
|
"base_fee_per_gas" => base_fee_per_gas, |
||||||
|
"priority_fee" => priority_fee_per_gas && Wei.mult(priority_fee_per_gas, transaction.gas_used), |
||||||
|
"tx_burnt_fee" => burned_fee, |
||||||
|
"nonce" => transaction.nonce, |
||||||
|
"position" => transaction.index, |
||||||
|
"revert_reason" => revert_reason, |
||||||
|
"raw_input" => transaction.input, |
||||||
|
"decoded_input" => decoded_input_data, |
||||||
|
"token_transfers" => token_transfers(transaction.token_transfers, conn, single_tx?), |
||||||
|
"token_transfers_overflow" => token_transfers_overflow(transaction.token_transfers, single_tx?), |
||||||
|
"exchange_rate" => (Market.get_exchange_rate(Explorer.coin()) || TokenRate.null()).usd_value, |
||||||
|
"method" => method_name(transaction, decoded_input), |
||||||
|
"tx_types" => tx_types(transaction), |
||||||
|
"tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(conn)) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
def token_transfers(_, _conn, false), do: nil |
||||||
|
def token_transfers(%NotLoaded{}, _conn, _), do: nil |
||||||
|
|
||||||
|
def token_transfers(token_transfers, conn, _) do |
||||||
|
render("token_transfers.json", %{ |
||||||
|
token_transfers: Enum.take(token_transfers, Chain.get_token_transfers_per_transaction_preview_count()), |
||||||
|
conn: conn |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
def token_transfers_overflow(_, false), do: nil |
||||||
|
def token_transfers_overflow(%NotLoaded{}, _), do: false |
||||||
|
|
||||||
|
def token_transfers_overflow(token_transfers, _), |
||||||
|
do: Enum.count(token_transfers) > Chain.get_token_transfers_per_transaction_preview_count() |
||||||
|
|
||||||
|
defp priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) do |
||||||
|
if is_nil(max_priority_fee_per_gas) or is_nil(base_fee_per_gas), |
||||||
|
do: nil, |
||||||
|
else: |
||||||
|
Enum.min_by([max_priority_fee_per_gas, Wei.sub(max_fee_per_gas, base_fee_per_gas)], fn x -> |
||||||
|
Wei.to(x, :wei) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp burned_fee(transaction, max_fee_per_gas, base_fee_per_gas) do |
||||||
|
if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do |
||||||
|
if Decimal.compare(max_fee_per_gas.value, 0) == :eq do |
||||||
|
%Wei{value: Decimal.new(0)} |
||||||
|
else |
||||||
|
Wei.mult(base_fee_per_gas, transaction.gas_used) |
||||||
|
end |
||||||
|
else |
||||||
|
nil |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp revert_reason(status, transaction) do |
||||||
|
if is_binary(status) && status |> String.downcase() |> String.contains?("reverted") do |
||||||
|
case TransactionView.transaction_revert_reason(transaction) do |
||||||
|
{:error, _contract_not_verified, candidates} when candidates != [] -> |
||||||
|
{:ok, method_id, text, mapping} = Enum.at(candidates, 0) |
||||||
|
render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: true) |
||||||
|
|
||||||
|
{:ok, method_id, text, mapping} -> |
||||||
|
render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: true) |
||||||
|
|
||||||
|
_ -> |
||||||
|
hex = TransactionView.get_pure_transaction_revert_reason(transaction) |
||||||
|
utf8 = TransactionView.decoded_revert_reason(transaction) |
||||||
|
render(__MODULE__, "revert_reason.json", raw: hex, decoded: utf8) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp decoded_input(decoded_input) do |
||||||
|
case decoded_input do |
||||||
|
{:ok, method_id, text, mapping} -> |
||||||
|
render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: false) |
||||||
|
|
||||||
|
_ -> |
||||||
|
nil |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_method_mapping(mapping) do |
||||||
|
Enum.map(mapping, fn {name, type, value} -> |
||||||
|
%{"name" => name, "type" => type, "value" => ABIEncodedValueView.value_json(type, value)} |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_log_mapping(mapping) do |
||||||
|
Enum.map(mapping, fn {name, type, indexed?, value} -> |
||||||
|
%{"name" => name, "type" => type, "indexed" => indexed?, "value" => ABIEncodedValueView.value_json(type, value)} |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp format_status({:error, reason}), do: reason |
||||||
|
defp format_status(status), do: status |
||||||
|
|
||||||
|
defp format_decoded_input({:error, _, []}), do: nil |
||||||
|
defp format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0) |
||||||
|
defp format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded |
||||||
|
defp format_decoded_input(_), do: nil |
||||||
|
|
||||||
|
defp format_decoded_log_input({:error, :could_not_decode}), do: nil |
||||||
|
defp format_decoded_log_input({:error, :no_matching_function}), do: nil |
||||||
|
defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded |
||||||
|
defp format_decoded_log_input({:error, _, candidates}), do: Enum.at(candidates, 0) |
||||||
|
|
||||||
|
def format_confirmations({:ok, confirmations}), do: confirmations |
||||||
|
def format_confirmations(_), do: 0 |
||||||
|
|
||||||
|
def format_fee({type, value}), do: %{"type" => type, "value" => value} |
||||||
|
|
||||||
|
def processing_time_duration(%Transaction{block: nil}) do |
||||||
|
[] |
||||||
|
end |
||||||
|
|
||||||
|
def processing_time_duration(%Transaction{earliest_processing_start: nil}) do |
||||||
|
avg_time = AverageBlockTime.average_block_time() |
||||||
|
|
||||||
|
if avg_time == {:error, :disabled} do |
||||||
|
[] |
||||||
|
else |
||||||
|
[ |
||||||
|
0, |
||||||
|
avg_time |
||||||
|
|> Duration.to_milliseconds() |
||||||
|
] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def processing_time_duration(%Transaction{ |
||||||
|
block: %Block{timestamp: end_time}, |
||||||
|
earliest_processing_start: earliest_processing_start, |
||||||
|
inserted_at: inserted_at |
||||||
|
}) do |
||||||
|
long_interval = abs(diff(earliest_processing_start, end_time)) |
||||||
|
short_interval = abs(diff(inserted_at, end_time)) |
||||||
|
merge_intervals(short_interval, long_interval) |
||||||
|
end |
||||||
|
|
||||||
|
def merge_intervals(short, long) when short == long, do: [short] |
||||||
|
|
||||||
|
def merge_intervals(short, long) do |
||||||
|
[short, long] |
||||||
|
end |
||||||
|
|
||||||
|
def diff(left, right) do |
||||||
|
left |
||||||
|
|> Timex.diff(right, :milliseconds) |
||||||
|
end |
||||||
|
|
||||||
|
defp method_name(_, {:ok, _method_id, text, _mapping}) do |
||||||
|
Transaction.parse_method_name(text, false) |
||||||
|
end |
||||||
|
|
||||||
|
defp method_name(%Transaction{to_address: to_address, input: %{bytes: <<method_id::binary-size(4), _::binary>>}}, _) do |
||||||
|
if Helper.is_smart_contract(to_address) do |
||||||
|
"0x" <> Base.encode16(method_id, case: :lower) |
||||||
|
else |
||||||
|
nil |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp method_name(_, _) do |
||||||
|
nil |
||||||
|
end |
||||||
|
|
||||||
|
defp tx_types(tx, types \\ [], stage \\ :token_transfer) |
||||||
|
|
||||||
|
defp tx_types(%Transaction{token_transfers: token_transfers} = tx, types, :token_transfer) do |
||||||
|
types = |
||||||
|
if !is_nil(token_transfers) && token_transfers != [] && !match?(%NotLoaded{}, token_transfers) do |
||||||
|
[:token_transfer | types] |
||||||
|
else |
||||||
|
types |
||||||
|
end |
||||||
|
|
||||||
|
tx_types(tx, types, :token_creation) |
||||||
|
end |
||||||
|
|
||||||
|
defp tx_types(%Transaction{created_contract_address: created_contract_address} = tx, types, :token_creation) do |
||||||
|
types = |
||||||
|
if match?(%Address{}, created_contract_address) && match?(%Token{}, created_contract_address.token) do |
||||||
|
[:token_creation | types] |
||||||
|
else |
||||||
|
types |
||||||
|
end |
||||||
|
|
||||||
|
tx_types(tx, types, :contract_creation) |
||||||
|
end |
||||||
|
|
||||||
|
defp tx_types( |
||||||
|
%Transaction{created_contract_address_hash: created_contract_address_hash} = tx, |
||||||
|
types, |
||||||
|
:contract_creation |
||||||
|
) do |
||||||
|
types = |
||||||
|
if is_nil(created_contract_address_hash) do |
||||||
|
types |
||||||
|
else |
||||||
|
[:contract_creation | types] |
||||||
|
end |
||||||
|
|
||||||
|
tx_types(tx, types, :contract_call) |
||||||
|
end |
||||||
|
|
||||||
|
defp tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do |
||||||
|
types = |
||||||
|
if Helper.is_smart_contract(to_address) do |
||||||
|
[:contract_call | types] |
||||||
|
else |
||||||
|
types |
||||||
|
end |
||||||
|
|
||||||
|
tx_types(tx, types, :coin_transfer) |
||||||
|
end |
||||||
|
|
||||||
|
defp tx_types(%Transaction{value: value}, types, :coin_transfer) do |
||||||
|
if Decimal.compare(value.value, 0) == :gt do |
||||||
|
[:coin_transfer | types] |
||||||
|
else |
||||||
|
types |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
defmodule Explorer.Repo.Migrations.AddMethodIdIndex do |
||||||
|
use Ecto.Migration |
||||||
|
|
||||||
|
@disable_ddl_transaction true |
||||||
|
|
||||||
|
def up do |
||||||
|
execute(""" |
||||||
|
CREATE INDEX CONCURRENTLY method_id ON public.transactions USING btree (substring(input for 4)); |
||||||
|
""") |
||||||
|
end |
||||||
|
|
||||||
|
def down do |
||||||
|
execute("DROP INDEX method_id") |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue