diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index 738095bec3..5c09ffba25 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- master
+ - api
env:
MIX_ENV: test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee0cf42202..bafd075d0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
### Features
+- [#6379](https://github.com/blockscout/blockscout/pull/6379) - API v2 for frontend
- [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var
- [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master
- [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles
diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex
index ca488dbcac..d850b6163e 100644
--- a/apps/block_scout_web/lib/block_scout_web/api_router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex
@@ -13,7 +13,7 @@ defmodule BlockScoutWeb.ApiRouter do
Router for API
"""
use BlockScoutWeb, :router
- alias BlockScoutWeb.Plug.CheckAccountAPI
+ alias BlockScoutWeb.Plug.{CheckAccountAPI, CheckApiV2}
pipeline :api do
plug(:accepts, ["json"])
@@ -25,9 +25,15 @@ defmodule BlockScoutWeb.ApiRouter do
plug(CheckAccountAPI)
end
+ pipeline :api_v2 do
+ plug(CheckApiV2)
+ plug(:fetch_session)
+ plug(:protect_from_forgery)
+ end
+
alias BlockScoutWeb.Account.Api.V1.{TagsController, UserController}
- scope "/account/v1" do
+ scope "/account/v1", as: :account_v1 do
pipe_through(:api)
pipe_through(:account_api)
@@ -83,13 +89,68 @@ defmodule BlockScoutWeb.ApiRouter do
end
end
+ scope "/v2", as: :api_v2 do
+ pipe_through(:api)
+ pipe_through(:api_v2)
+
+ alias BlockScoutWeb.API.V2
+
+ get("/search", V2.SearchController, :search)
+
+ scope "/config" do
+ get("/json-rpc-url", V2.ConfigController, :json_rpc_url)
+ end
+
+ scope "/transactions" do
+ get("/", V2.TransactionController, :transactions)
+ get("/:transaction_hash", V2.TransactionController, :transaction)
+ get("/:transaction_hash/token-transfers", V2.TransactionController, :token_transfers)
+ get("/:transaction_hash/internal-transactions", V2.TransactionController, :internal_transactions)
+ get("/:transaction_hash/logs", V2.TransactionController, :logs)
+ get("/:transaction_hash/raw-trace", V2.TransactionController, :raw_trace)
+ end
+
+ scope "/blocks" do
+ get("/", V2.BlockController, :blocks)
+ get("/:block_hash_or_number", V2.BlockController, :block)
+ get("/:block_hash_or_number/transactions", V2.BlockController, :transactions)
+ end
+
+ scope "/addresses" do
+ get("/:address_hash", V2.AddressController, :address)
+ get("/:address_hash/token-balances", V2.AddressController, :token_balances)
+ get("/:address_hash/transactions", V2.AddressController, :transactions)
+ get("/:address_hash/token-transfers", V2.AddressController, :token_transfers)
+ get("/:address_hash/internal-transactions", V2.AddressController, :internal_transactions)
+ get("/:address_hash/logs", V2.AddressController, :logs)
+ get("/:address_hash/blocks-validated", V2.AddressController, :blocks_validated)
+ get("/:address_hash/coin-balance-history", V2.AddressController, :coin_balance_history)
+ get("/:address_hash/coin-balance-history-by-day", V2.AddressController, :coin_balance_history_by_day)
+ end
+
+ scope "/main-page" do
+ get("/blocks", V2.MainPageController, :blocks)
+ get("/transactions", V2.MainPageController, :transactions)
+ end
+
+ scope "/stats" do
+ get("/", V2.StatsController, :stats)
+
+ scope "/charts" do
+ get("/transactions", V2.StatsController, :transactions_chart)
+ get("/market", V2.StatsController, :market_chart)
+ end
+ end
+ end
+
scope "/v1", as: :api_v1 do
pipe_through(:api)
alias BlockScoutWeb.API.{EthRPC, RPC, V1}
alias BlockScoutWeb.API.V1.HealthController
- alias BlockScoutWeb.SearchController
+ alias BlockScoutWeb.API.V2.SearchController
- get("/search", SearchController, :api_search_result)
+ # leave the same endpoint in v1 in order to keep backward compatibility
+ get("/search", SearchController, :search)
get("/health", HealthController, :health)
get("/gas-price-oracle", V1.GasPriceOracleController, :gas_price_oracle)
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
index 2a2816d8be..e73a54dff9 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
@@ -4,6 +4,8 @@ defmodule BlockScoutWeb.AddressChannel do
"""
use BlockScoutWeb, :channel
+ alias BlockScoutWeb.API.V2.AddressView, as: AddressViewAPI
+
alias BlockScoutWeb.{
AddressCoinBalanceView,
AddressView,
@@ -56,6 +58,20 @@ defmodule BlockScoutWeb.AddressChannel do
end
end
+ def handle_out(
+ "balance_update",
+ %{address: address, exchange_rate: exchange_rate},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "balance", %{
+ balance: address.fetched_coin_balance.value,
+ block_number: address.fetched_coin_balance_block_number,
+ exchange_rate: exchange_rate.usd_value
+ })
+
+ {:noreply, socket}
+ end
+
def handle_out(
"balance_update",
%{address: address, exchange_rate: exchange_rate},
@@ -88,6 +104,12 @@ defmodule BlockScoutWeb.AddressChannel do
end
end
+ def handle_out("count", %{count: count}, %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket) do
+ push(socket, "count", %{count: to_string(count)})
+
+ {:noreply, socket}
+ end
+
def handle_out("count", %{count: count}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@@ -96,6 +118,16 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket}
end
+ def handle_out(
+ "internal_transaction",
+ %{address: _address, internal_transaction: _internal_transaction},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "internal_transaction", %{internal_transaction: 1})
+
+ {:noreply, socket}
+ end
+
def handle_out("internal_transaction", %{address: address, internal_transaction: internal_transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@@ -120,6 +152,22 @@ defmodule BlockScoutWeb.AddressChannel do
def handle_out("token_transfer", data, socket), do: handle_token_transfer(data, socket, "token_transfer")
+ def handle_out(
+ "coin_balance",
+ %{block_number: block_number},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number)
+
+ rendered_coin_balance = AddressViewAPI.render("coin_balance.json", %{coin_balance: coin_balance})
+
+ push(socket, "coin_balance", %{coin_balance: rendered_coin_balance})
+
+ push_current_coin_balance(socket, block_number, coin_balance)
+
+ {:noreply, socket}
+ end
+
def handle_out("coin_balance", %{block_number: block_number}, socket) do
coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number)
@@ -142,8 +190,23 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket}
end
+ def handle_out("pending_transaction", data, %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket),
+ do: handle_transaction(data, socket, "pending_transaction")
+
def handle_out("pending_transaction", data, socket), do: handle_transaction(data, socket, "transaction")
+ def push_current_coin_balance(
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
+ block_number,
+ coin_balance
+ ) do
+ push(socket, "current_coin_balance", %{
+ coin_balance: (coin_balance && coin_balance.value) || %Wei{value: Decimal.new(0)},
+ exchange_rate: (Market.get_exchange_rate(Explorer.coin()) || Token.null()).usd_value,
+ block_number: block_number
+ })
+ end
+
def push_current_coin_balance(socket, block_number, coin_balance) do
{:ok, hash} = Chain.string_to_address_hash(socket.assigns.address_hash)
@@ -172,6 +235,16 @@ defmodule BlockScoutWeb.AddressChannel do
})
end
+ def handle_transaction(
+ %{address: _address, transaction: _transaction},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
+ event
+ ) do
+ push(socket, event, %{transaction: 1})
+
+ {:noreply, socket}
+ end
+
def handle_transaction(%{address: address, transaction: transaction}, socket, event) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@@ -195,6 +268,16 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket}
end
+ def handle_token_transfer(
+ %{address: _address, token_transfer: _token_transfer},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
+ event
+ ) do
+ push(socket, event, %{token_transfer: 1})
+
+ {:noreply, socket}
+ end
+
def handle_token_transfer(%{address: address, token_transfer: token_transfer}, socket, event) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/block_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/block_channel.ex
index cea7e44104..560a1d4d96 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/block_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/block_channel.ex
@@ -4,8 +4,10 @@ defmodule BlockScoutWeb.BlockChannel do
"""
use BlockScoutWeb, :channel
+ alias BlockScoutWeb.API.V2.BlockView, as: BlockViewAPI
alias BlockScoutWeb.{BlockView, ChainView}
alias Phoenix.View
+ alias Timex.Duration
intercept(["new_block"])
@@ -17,6 +19,21 @@ defmodule BlockScoutWeb.BlockChannel do
{:ok, %{}, socket}
end
+ def handle_out(
+ "new_block",
+ %{block: block, average_block_time: average_block_time},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ rendered_block = BlockViewAPI.render("block.json", %{block: block, socket: nil})
+
+ push(socket, "new_block", %{
+ average_block_time: to_string(Duration.to_milliseconds(average_block_time)),
+ block: rendered_block
+ })
+
+ {:noreply, socket}
+ end
+
def handle_out("new_block", %{block: block, average_block_time: average_block_time}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/exchange_rate_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/exchange_rate_channel.ex
index 13569b8399..ef5d5208e3 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/exchange_rate_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/exchange_rate_channel.ex
@@ -10,6 +10,20 @@ defmodule BlockScoutWeb.ExchangeRateChannel do
{:ok, %{}, socket}
end
+ def handle_out(
+ "new_rate",
+ %{exchange_rate: exchange_rate, market_history_data: market_history_data},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "new_rate", %{
+ exchange_rate: exchange_rate.usd_value,
+ available_supply: exchange_rate.available_supply,
+ chart_data: market_history_data
+ })
+
+ {:noreply, socket}
+ end
+
def handle_out("new_rate", %{exchange_rate: exchange_rate, market_history_data: market_history_data}, socket) do
push(socket, "new_rate", %{
exchange_rate: exchange_rate,
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
index 6f6ff3a70b..e53ac616e7 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
@@ -17,6 +17,16 @@ defmodule BlockScoutWeb.RewardChannel do
end
end
+ def handle_out(
+ "new_reward",
+ %{emission_funds: _emission_funds, validator: _validator},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "new_reward", %{reward: 1})
+
+ {:noreply, socket}
+ end
+
def handle_out("new_reward", %{emission_funds: emission_funds, validator: validator}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/token_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/token_channel.ex
index 2d3ef58d3b..56b22c06f3 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/token_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/token_channel.ex
@@ -14,12 +14,18 @@ defmodule BlockScoutWeb.TokenChannel do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash
- def join("tokens:new_token_transfer", _params, socket) do
+ def join("tokens:" <> _transaction_hash, _params, socket) do
{:ok, %{}, socket}
end
- def join("tokens:" <> _transaction_hash, _params, socket) do
- {:ok, %{}, socket}
+ def handle_out(
+ "token_transfer",
+ %{token_transfer: _token_transfer},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "token_transfer", %{token_transfer: 1})
+
+ {:noreply, socket}
end
def handle_out("token_transfer", %{token_transfer: token_transfer}, socket) do
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex
index ac1085ea29..669991e041 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex
@@ -30,6 +30,16 @@ defmodule BlockScoutWeb.TransactionChannel do
{:ok, %{}, socket}
end
+ def handle_out(
+ "pending_transaction",
+ %{transaction: _transaction},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "pending_transaction", %{pending_transaction: 1})
+
+ {:noreply, socket}
+ end
+
def handle_out("pending_transaction", %{transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@@ -50,6 +60,16 @@ defmodule BlockScoutWeb.TransactionChannel do
{:noreply, socket}
end
+ def handle_out(
+ "transaction",
+ %{transaction: _transaction},
+ %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
+ ) do
+ push(socket, "transaction", %{transaction: 1})
+
+ {:noreply, socket}
+ end
+
def handle_out("transaction", %{transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex b/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex
new file mode 100644
index 0000000000..b012b8db2a
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
new file mode 100644
index 0000000000..717210dc50
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
new file mode 100644
index 0000000000..24a3daf808
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex
new file mode 100644
index 0000000000..9d76eee674
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex
new file mode 100644
index 0000000000..20f900066b
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
new file mode 100644
index 0000000000..417eaf4009
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex
new file mode 100644
index 0000000000..59b06a3d77
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
new file mode 100644
index 0000000000..fa1a46a0c4
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
new file mode 100644
index 0000000000..cce9bd7718
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
index 9602aae554..facda14c92 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
@@ -133,7 +133,7 @@ defmodule BlockScoutWeb.BlockTransactionController do
end
end
- defp param_block_hash_or_number_to_block("0x" <> _ = param, options) do
+ def param_block_hash_or_number_to_block("0x" <> _ = param, options) do
case string_to_block_hash(param) do
{:ok, hash} ->
hash_to_block(hash, options)
@@ -143,8 +143,8 @@ defmodule BlockScoutWeb.BlockTransactionController do
end
end
- defp param_block_hash_or_number_to_block(number_string, options)
- when is_binary(number_string) do
+ def param_block_hash_or_number_to_block(number_string, options)
+ when is_binary(number_string) do
case BlockScoutWeb.Chain.param_to_block_number(number_string) do
{:ok, number} ->
number_to_block(number, options)
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex
index a52a6dd750..dec6f4f90e 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex
@@ -5,7 +5,7 @@ defmodule BlockScoutWeb.Chain.TransactionHistoryChartController do
def show(conn, _params) do
if ajax?(conn) do
- [{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, 30)
+ [{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, [{:history_size, 30}])
today = Date.utc_today()
latest = Date.add(today, -1)
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
index 6d112bdc45..61ac896135 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
@@ -3,6 +3,7 @@ defmodule BlockScoutWeb.ChainController do
import BlockScoutWeb.Chain, only: [paging_options: 1]
+ alias BlockScoutWeb.API.V2.Helper
alias BlockScoutWeb.{ChainView, Controller}
alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.Chain.{Address, Block, Transaction}
@@ -10,7 +11,6 @@ defmodule BlockScoutWeb.ChainController do
alias Explorer.Chain.Cache.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 Explorer.Market
@@ -33,7 +33,7 @@ defmodule BlockScoutWeb.ChainController do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
- transaction_stats = get_transaction_stats()
+ transaction_stats = Helper.get_transaction_stats()
chart_data_paths = %{
market: market_history_chart_path(conn, :show),
@@ -61,25 +61,6 @@ defmodule BlockScoutWeb.ChainController do
)
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
-
def search(conn, %{"q" => ""}) do
show(conn, [])
end
@@ -110,7 +91,7 @@ defmodule BlockScoutWeb.ChainController do
results =
paging_options
- |> search_by(offset, term)
+ |> Chain.joint_search(offset, term)
encoded_results =
results
@@ -144,10 +125,6 @@ defmodule BlockScoutWeb.ChainController do
json(conn, "{}")
end
- def search_by(paging_options, offset, term) do
- Chain.joint_search(paging_options, offset, term)
- end
-
def chain_blocks(conn, _params) do
if ajax?(conn) do
blocks =
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pagination_helpers.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pagination_helpers.ex
deleted file mode 100644
index e5ad7838bd..0000000000
--- a/apps/block_scout_web/lib/block_scout_web/controllers/pagination_helpers.ex
+++ /dev/null
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
index 55fe550745..906fbc3194 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
@@ -63,7 +63,7 @@ defmodule BlockScoutWeb.PendingTransactionController do
end
defp get_pending_transactions_and_next_page(options) do
- transactions_plus_one = Chain.recent_pending_transactions(options)
+ transactions_plus_one = Chain.recent_pending_transactions(options, true)
split_list_by_page(transactions_plus_one)
end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
index 714c9e351d..d692d6de14 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
@@ -11,7 +11,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do
def index(conn, _params) do
if ajax?(conn) do
recent_transactions =
- Chain.recent_collated_transactions(
+ Chain.recent_collated_transactions(true,
necessity_by_association: %{
:block => :required,
[created_contract_address: :names] => :optional,
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/search_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/search_controller.ex
index 87dfceeaf6..b2f639a6e3 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/search_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/search_controller.ex
@@ -3,7 +3,8 @@ defmodule BlockScoutWeb.SearchController do
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
- alias BlockScoutWeb.{ChainController, Controller, SearchView}
+ alias BlockScoutWeb.{Controller, SearchView}
+ alias Explorer.Chain
alias Phoenix.View
def search_results(conn, %{"q" => query, "type" => "JSON"} = params) do
@@ -12,7 +13,7 @@ defmodule BlockScoutWeb.SearchController do
search_results_plus_one =
paging_options
- |> ChainController.search_by(offset, query)
+ |> Chain.joint_search(offset, query)
{search_results, next_page} = split_list_by_page(search_results_plus_one)
@@ -73,21 +74,4 @@ defmodule BlockScoutWeb.SearchController do
current_path: Controller.current_full_path(conn)
)
end
-
- def api_search_result(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
- |> ChainController.search_by(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
diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex
index 0c85b34e67..19c6163dad 100644
--- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex
+++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex
@@ -7,6 +7,7 @@ defmodule BlockScoutWeb.Endpoint do
end
socket("/socket", BlockScoutWeb.UserSocket, websocket: [timeout: 45_000])
+ socket("/socket/v2", BlockScoutWeb.UserSocketV2, websocket: [timeout: 45_000])
# Serve at "/" the static files from "priv/static" directory.
#
diff --git a/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex b/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
index bbb02d9bdd..8f0283cbda 100644
--- a/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
+++ b/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
@@ -6,14 +6,13 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
import Ecto.Query, only: [from: 2]
alias Explorer.Account.{TagAddress, WatchlistAddress}
- alias Explorer.Chain.Hash
alias Explorer.Repo
alias Explorer.Tags.{AddressTag, AddressToTag}
def get_address_tags(nil, nil),
do: %{common_tags: [], personal_tags: [], watchlist_names: []}
- def get_address_tags(%Hash{} = address_hash, current_user) do
+ def get_address_tags(address_hash, current_user) when not is_nil(address_hash) do
%{
common_tags: get_tags_on_address(address_hash),
personal_tags: get_personal_tags(address_hash, current_user),
@@ -23,13 +22,13 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_address_tags(_, _), do: %{common_tags: [], personal_tags: [], watchlist_names: []}
- def get_public_tags(%Hash{} = address_hash) do
+ def get_public_tags(address_hash) when not is_nil(address_hash) do
%{
common_tags: get_tags_on_address(address_hash)
}
end
- def get_tags_on_address(%Hash{} = address_hash) do
+ def get_tags_on_address(address_hash) when not is_nil(address_hash) do
query =
from(
tt in AddressTag,
@@ -45,7 +44,7 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_tags_on_address(_), do: []
- def get_personal_tags(%Hash{} = address_hash, %{id: id}) do
+ def get_personal_tags(address_hash, %{id: id}) when not is_nil(address_hash) do
query =
from(
ta in TagAddress,
@@ -59,7 +58,7 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_personal_tags(_, _), do: []
- def get_watchlist_names_on_address(%Hash{} = address_hash, %{watchlist_id: watchlist_id}) do
+ def get_watchlist_names_on_address(address_hash, %{watchlist_id: watchlist_id}) when not is_nil(address_hash) do
query =
from(
wa in WatchlistAddress,
diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex
index 286d911453..f940fbec1d 100644
--- a/apps/block_scout_web/lib/block_scout_web/notifier.ex
+++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex
@@ -184,7 +184,7 @@ defmodule BlockScoutWeb.Notifier do
today = Date.utc_today()
[{:history_size, history_size}] =
- Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, 30)
+ Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, {:history_size, 30})
x_days_back = Date.add(today, -1 * history_size)
@@ -371,8 +371,6 @@ defmodule BlockScoutWeb.Notifier do
end
defp broadcast_token_transfer(token_transfer, event) do
- Endpoint.broadcast("token_transfers:#{token_transfer.transaction_hash}", event, %{})
-
Endpoint.broadcast("addresses:#{token_transfer.from_address_hash}", event, %{
address: token_transfer.from_address,
token_transfer: token_transfer
diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex
new file mode 100644
index 0000000000..3f44c6d62a
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/plug/check_api_v2.ex b/apps/block_scout_web/lib/block_scout_web/plug/check_api_v2.ex
new file mode 100644
index 0000000000..95269a2039
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/plug/check_api_v2.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
index aad847fb1c..7691bb3aed 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
@@ -17,7 +17,7 @@
<%= link(
to: address_token_transfers_path(@conn, :index, to_string(@address.hash), to_string(@token.contract_address_hash)),
class: "tile-title-lg",
- "data-test": "token_transfers_#{@token_balance.token.contract_address_hash}"
+ "data-test": "token_transfers_#{@token.contract_address_hash}"
) do %>
<%= token_name(@token) %>
<% end %>
@@ -33,14 +33,14 @@
- <% token_price = if @token_balance.token.usd_value, do: @token_balance.token.usd_value, else: nil %>
- <%= ChainView.format_currency_value(token_price, "@") %>
+ <% token_price = if @token.usd_value, do: @token.usd_value, else: nil %>
+ <%= ChainView.format_currency_value(token_price, "@") %>
|
- <%= if @token_balance.token.usd_value do %>
+ <%= if @token.usd_value do %>
- <%= ChainView.format_usd_value(Chain.balance_in_usd(@token_balance)) %>
+ <%= ChainView.format_usd_value(Chain.balance_in_usd(@token_balance, @token)) %>
<% end %>
|
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
index d0c1adf636..9b3f6a70f7 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
@@ -47,7 +47,7 @@
placeholder: gettext("Search tokens")
) %>
- <%= if Enum.any?(@token_balances, fn {token_balance, _} -> token_balance.token.type == "ERC-721" end) do %>
+ <%= if Enum.any?(@token_balances, fn {_token_balance, token} -> token.type == "ERC-721" end) do %>
<%= render(
"_tokens.html",
conn: @conn,
@@ -56,7 +56,7 @@
) %>
<% end %>
- <%= if Enum.any?(@token_balances, fn {token_balance, _} -> token_balance.token.type == "ERC-1155" end) do %>
+ <%= if Enum.any?(@token_balances, fn {_token_balance, token} -> token.type == "ERC-1155" end) do %>
<%= render(
"_tokens.html",
conn: @conn,
@@ -65,7 +65,7 @@
) %>
<% end %>
- <%= if Enum.any?(@token_balances, fn {token_balance, _} -> token_balance.token.type == "ERC-20" end) do %>
+ <%= if Enum.any?(@token_balances, fn {_token_balance, token} -> token.type == "ERC-20" end) do %>
<%= render(
"_tokens.html",
conn: @conn,
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
index 753460e78f..ceaf90505a 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
@@ -7,13 +7,13 @@
<% path = cond do
- token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id))
- token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id))
- true -> token_path(@conn, :show, to_string(token_balance.token.contract_address_hash))
+ token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token.contract_address_hash, to_string(token_balance.token_id))
+ token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token.contract_address_hash, to_string(token_balance.token_id))
+ true -> token_path(@conn, :show, to_string(token.contract_address_hash))
end
%>
<%= link(
@@ -34,16 +34,16 @@
<% end %>
"><%= token_name(token) %>
- <%= if token_balance.token.usd_value do %>
+ <%= if token.usd_value do %>
-
+
<% end %>
- <%= if token_balance.token.usd_value do %>
+ <%= if token.usd_value do %>
-
+
<% end %>
@@ -52,7 +52,7 @@
<%= if token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) do %>
1
<% else %>
- <%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_symbol(token_balance.token) %>
+ <%= format_according_to_decimals(token_balance.value, token.decimals) %> <%= token_symbol(token) %>
<% end %>
<%= if (token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id)) or token_balance.token_type == "ERC-1155" do %>
<%= " TokenID " <> to_string(token_balance.token_id) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex
index 0f09ce2302..77f4fa3e05 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex
@@ -27,6 +27,19 @@ defmodule BlockScoutWeb.ABIEncodedValueView do
:error
end
+ def value_json(type, value) do
+ decoded_type = FunctionSelector.decode_type(type)
+
+ do_value_json(decoded_type, value)
+ rescue
+ exception ->
+ Logger.warn(fn ->
+ ["Error determining value json for #{inspect(type)}: ", Exception.format(:error, exception)]
+ end)
+
+ nil
+ end
+
def copy_text(type, value) do
decoded_type = FunctionSelector.decode_type(type)
@@ -145,5 +158,56 @@ defmodule BlockScoutWeb.ABIEncodedValueView do
defp base_value_html(_, value, _no_links), do: HTML.html_escape(value)
+ defp do_value_json({:bytes, _}, value) do
+ do_value_json(:bytes, value)
+ end
+
+ defp do_value_json({:array, type, _}, value) do
+ do_value_json({:array, type}, value)
+ end
+
+ defp do_value_json({:array, type}, value) do
+ values =
+ Enum.map(value, fn inner_value ->
+ do_value_json(type, inner_value)
+ end)
+
+ values
+ end
+
+ defp do_value_json({:tuple, types}, values) do
+ values_list =
+ values
+ |> Tuple.to_list()
+ |> Enum.with_index()
+ |> Enum.map(fn {value, i} ->
+ do_value_json(Enum.at(types, i), value)
+ end)
+
+ values_list
+ end
+
+ defp do_value_json(type, value) do
+ base_value_json(type, value)
+ end
+
+ defp base_value_json(_, {:dynamic, value}) do
+ hex(value)
+ end
+
+ defp base_value_json(:address, value) do
+ hex(value)
+ end
+
+ defp base_value_json(:address_text, value) do
+ hex(value)
+ end
+
+ defp base_value_json(:bytes, value) do
+ hex(value)
+ end
+
+ defp base_value_json(_, value), do: value
+
defp hex(value), do: "0x" <> Base.encode16(value, case: :lower)
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
index 47d6525b04..d070ddf2fd 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
@@ -11,7 +11,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
end
def filter_by_type(token_balances, type) do
- Enum.filter(token_balances, fn {token_balance, _} -> token_balance.token.type == type end)
+ Enum.filter(token_balances, fn {_token_balance, token} -> token.type == type end)
end
@doc """
@@ -30,15 +30,23 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
def sort_by_usd_value_and_name(token_balances) do
token_balances
|> Enum.sort(fn {token_balance1, token1}, {token_balance2, token2} ->
- usd_value1 = token_balance1.token.usd_value
- usd_value2 = token_balance2.token.usd_value
+ usd_value1 = token1.usd_value
+ usd_value2 = token2.usd_value
token_name1 = token1.name
token_name2 = token2.name
sort_by_name = sort_2_tokens_by_name(token_name1, token_name2)
- sort_2_tokens_by_value_desc_and_name(token_balance1, token_balance2, usd_value1, usd_value2, sort_by_name)
+ sort_2_tokens_by_value_desc_and_name(
+ token_balance1,
+ token_balance2,
+ usd_value1,
+ usd_value2,
+ sort_by_name,
+ token1,
+ token2
+ )
end)
end
@@ -58,9 +66,17 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
end
end
- defp sort_2_tokens_by_value_desc_and_name(token_balance1, token_balance2, usd_value1, usd_value2, sort_by_name)
+ defp sort_2_tokens_by_value_desc_and_name(
+ token_balance1,
+ token_balance2,
+ usd_value1,
+ usd_value2,
+ sort_by_name,
+ token1,
+ token2
+ )
when not is_nil(usd_value1) and not is_nil(usd_value2) do
- case Decimal.compare(Chain.balance_in_usd(token_balance1), Chain.balance_in_usd(token_balance2)) do
+ case Decimal.compare(Chain.balance_in_usd(token_balance1, token1), Chain.balance_in_usd(token_balance2, token2)) do
:gt ->
true
@@ -72,17 +88,41 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
end
end
- defp sort_2_tokens_by_value_desc_and_name(_token_balance1, _token_balance2, usd_value1, usd_value2, _sort_by_name)
+ defp sort_2_tokens_by_value_desc_and_name(
+ _token_balance1,
+ _token_balance2,
+ usd_value1,
+ usd_value2,
+ _sort_by_name,
+ _token1,
+ _token2
+ )
when not is_nil(usd_value1) and is_nil(usd_value2) do
true
end
- defp sort_2_tokens_by_value_desc_and_name(_token_balance1, _token_balance2, usd_value1, usd_value2, _sort_by_name)
+ defp sort_2_tokens_by_value_desc_and_name(
+ _token_balance1,
+ _token_balance2,
+ usd_value1,
+ usd_value2,
+ _sort_by_name,
+ _token1,
+ _token2
+ )
when is_nil(usd_value1) and not is_nil(usd_value2) do
false
end
- defp sort_2_tokens_by_value_desc_and_name(_token_balance1, _token_balance2, usd_value1, usd_value2, sort_by_name)
+ defp sort_2_tokens_by_value_desc_and_name(
+ _token_balance1,
+ _token_balance2,
+ usd_value1,
+ usd_value2,
+ sort_by_name,
+ _token1,
+ _token2
+ )
when is_nil(usd_value1) and is_nil(usd_value2) do
sort_by_name
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
new file mode 100644
index 0000000000..54511abc00
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_v2.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_v2.ex
new file mode 100644
index 0000000000..6bd12a4103
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_v2.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex
new file mode 100644
index 0000000000..1fde984d4a
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex
@@ -0,0 +1,7 @@
+defmodule BlockScoutWeb.API.V2.ApiView do
+ def render("message.json", %{message: message}) do
+ %{
+ "message" => message
+ }
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex
new file mode 100644
index 0000000000..bddcddf089
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex
new file mode 100644
index 0000000000..be0d9da2c7
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
new file mode 100644
index 0000000000..57f914bc0c
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
new file mode 100644
index 0000000000..434a6542b4
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
new file mode 100644
index 0000000000..e0bdae9c98
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
new file mode 100644
index 0000000000..8b6c48f5b8
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
@@ -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: <>}}, _) 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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex
index 78a22e7670..7fdabe4b5e 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex
@@ -3,6 +3,7 @@ defmodule BlockScoutWeb.BlockView do
import Math.Enum, only: [mean: 1]
+ alias Ecto.Association.NotLoaded
alias Explorer.Chain
alias Explorer.Chain.{Block, Wei}
alias Explorer.Chain.Block.Reward
@@ -23,6 +24,7 @@ defmodule BlockScoutWeb.BlockView do
"#{average} #{unit_text}"
end
+ def block_type(%Block{consensus: false, nephews: %NotLoaded{}}), do: "Reorg"
def block_type(%Block{consensus: false, nephews: []}), do: "Reorg"
def block_type(%Block{consensus: false}), do: "Uncle"
def block_type(_block), do: "Block"
diff --git a/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex b/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
index ff5a737bfb..43d66a2926 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
@@ -3,27 +3,11 @@ defmodule BlockScoutWeb.ChainView do
require Decimal
import Number.Currency, only: [number_to_currency: 2]
+ import BlockScoutWeb.API.V2.Helper, only: [market_cap: 2]
alias BlockScoutWeb.LayoutView
alias Explorer.Chain.Cache.GasPriceOracle
- defp 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
-
- defp market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value}) do
- Decimal.mult(available_supply, usd_value)
- end
-
- defp market_cap(:standard, exchange_rate) do
- exchange_rate.market_cap_usd
- end
-
- defp market_cap(module, exchange_rate) do
- module.market_cap(exchange_rate)
- end
-
def format_usd_value(nil), do: ""
def format_usd_value(value) do
diff --git a/apps/block_scout_web/lib/block_scout_web/views/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/search_view.ex
index 4cab5f1f54..51bf1b856d 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/search_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/search_view.ex
@@ -16,52 +16,4 @@ defmodule BlockScoutWeb.SearchView do
|> Regex.replace(safe_result, "\\g{0}", global: true)
|> raw()
end
-
- 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(BlockScoutWeb.Endpoint, :show, search_result.address_hash),
- "address_url" => address_path(BlockScoutWeb.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(BlockScoutWeb.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(BlockScoutWeb.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(BlockScoutWeb.Endpoint, :show, tx_hash)
- }
- end
-
- defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
index 7b8d3276d6..40d5f4e16e 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
@@ -52,6 +52,56 @@ defmodule BlockScoutWeb.Tokens.Helpers do
nil
end
+ def token_transfer_amount_for_api(%{
+ token: token,
+ amount: amount,
+ amounts: amounts,
+ token_id: token_id,
+ token_ids: token_ids
+ }) do
+ do_token_transfer_amount_for_api(token, amount, amounts, token_id, token_ids)
+ end
+
+ def token_transfer_amount_for_api(%{token: token, amount: amount, token_id: token_id}) do
+ do_token_transfer_amount_for_api(token, amount, nil, token_id, nil)
+ end
+
+ defp do_token_transfer_amount_for_api(%Token{type: "ERC-20"}, nil, nil, _token_id, _token_ids) do
+ {:ok, nil}
+ end
+
+ defp do_token_transfer_amount_for_api(
+ %Token{type: "ERC-20", decimals: decimals},
+ amount,
+ _amounts,
+ _token_id,
+ _token_ids
+ ) do
+ {:ok, amount, decimals}
+ end
+
+ defp do_token_transfer_amount_for_api(%Token{type: "ERC-721"}, _amount, _amounts, _token_id, _token_ids) do
+ {:ok, :erc721_instance}
+ end
+
+ defp do_token_transfer_amount_for_api(
+ %Token{type: "ERC-1155", decimals: decimals},
+ amount,
+ amounts,
+ _token_id,
+ token_ids
+ ) do
+ if amount do
+ {:ok, :erc1155_instance, amount, decimals}
+ else
+ {:ok, :erc1155_instance, amounts, token_ids, decimals}
+ end
+ end
+
+ defp do_token_transfer_amount_for_api(_token, _amount, _amounts, _token_id, _token_ids) do
+ nil
+ end
+
@doc """
Returns the token's symbol.
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index 662e1cee46..213db8e577 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -512,7 +512,7 @@ msgstr ""
msgid "Chat (#blockscout)"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:63
+#: lib/block_scout_web/views/block_view.ex:65
#, elixir-autogen, elixir-format
msgid "Chore Reward"
msgstr ""
@@ -1105,7 +1105,7 @@ msgstr ""
msgid "Emission Contract"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:71
+#: lib/block_scout_web/views/block_view.ex:73
#, elixir-autogen, elixir-format
msgid "Emission Reward"
msgstr ""
@@ -1323,7 +1323,7 @@ msgstr ""
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38
-#: lib/block_scout_web/views/block_view.ex:21
+#: lib/block_scout_web/views/block_view.ex:22
#: lib/block_scout_web/views/wei_helpers.ex:77
#, elixir-autogen, elixir-format
msgid "Gwei"
@@ -1596,8 +1596,8 @@ msgstr ""
msgid "Miner"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:61
-#: lib/block_scout_web/views/block_view.ex:66
+#: lib/block_scout_web/views/block_view.ex:63
+#: lib/block_scout_web/views/block_view.ex:68
#, elixir-autogen, elixir-format
msgid "Miner Reward"
msgstr ""
@@ -2823,7 +2823,7 @@ msgstr ""
msgid "UTF-8"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:75
+#: lib/block_scout_web/views/block_view.ex:77
#, elixir-autogen, elixir-format
msgid "Uncle Reward"
msgstr ""
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index 12435bfd93..83ed5290b3 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -512,7 +512,7 @@ msgstr ""
msgid "Chat (#blockscout)"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:63
+#: lib/block_scout_web/views/block_view.ex:65
#, elixir-autogen, elixir-format
msgid "Chore Reward"
msgstr ""
@@ -1105,7 +1105,7 @@ msgstr ""
msgid "Emission Contract"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:71
+#: lib/block_scout_web/views/block_view.ex:73
#, elixir-autogen, elixir-format
msgid "Emission Reward"
msgstr ""
@@ -1323,7 +1323,7 @@ msgstr ""
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38
-#: lib/block_scout_web/views/block_view.ex:21
+#: lib/block_scout_web/views/block_view.ex:22
#: lib/block_scout_web/views/wei_helpers.ex:77
#, elixir-autogen, elixir-format
msgid "Gwei"
@@ -1596,8 +1596,8 @@ msgstr ""
msgid "Miner"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:61
-#: lib/block_scout_web/views/block_view.ex:66
+#: lib/block_scout_web/views/block_view.ex:63
+#: lib/block_scout_web/views/block_view.ex:68
#, elixir-autogen, elixir-format
msgid "Miner Reward"
msgstr ""
@@ -2823,7 +2823,7 @@ msgstr ""
msgid "UTF-8"
msgstr ""
-#: lib/block_scout_web/views/block_view.ex:75
+#: lib/block_scout_web/views/block_view.ex:77
#, elixir-autogen, elixir-format
msgid "Uncle Reward"
msgstr ""
diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
index 3a14474c2c..5216623e71 100644
--- a/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
@@ -133,7 +133,7 @@ defmodule BlockScoutWeb.ViewingChainTest do
transaction =
:transaction
|> insert(to_address: contract_token_address)
- |> with_block(block)
+ |> with_block(block, status: :ok)
insert_list(
3,
diff --git a/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
index cd57a45f95..1ac75c92b4 100644
--- a/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
@@ -20,9 +20,11 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
token_balance_a = build(:token_balance, token: build(:token, type: "ERC-20"))
token_balance_b = build(:token_balance, token: build(:token, type: "ERC-721"))
- token_balances = [{token_balance_a, %{}}, {token_balance_b, %{}}]
+ token_balances = [{token_balance_a, token_balance_a.token}, {token_balance_b, token_balance_b.token}]
- assert AddressTokenBalanceView.filter_by_type(token_balances, "ERC-20") == [{token_balance_a, %{}}]
+ assert AddressTokenBalanceView.filter_by_type(token_balances, "ERC-20") == [
+ {token_balance_a, token_balance_a.token}
+ ]
end
end
diff --git a/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs
index 5336dc9ff7..eea5baf284 100644
--- a/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs
@@ -34,8 +34,9 @@ defmodule BlockScoutWeb.BlockViewTest do
test "returns Uncle" do
uncle = insert(:block, consensus: false)
insert(:block_second_degree_relation, uncle_hash: uncle.hash)
+ preloaded = Repo.preload(uncle, :nephews)
- assert BlockView.block_type(uncle) == "Uncle"
+ assert BlockView.block_type(preloaded) == "Uncle"
end
end
diff --git a/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs b/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs
index 8b6096658a..335844236e 100644
--- a/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs
+++ b/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs
@@ -8,7 +8,7 @@ alias Explorer.Chain.Block
Benchee.run(
%{
"Explorer.Chain.recent_collated_transactions" => fn _ ->
- Chain.recent_collated_transactions()
+ Chain.recent_collated_transactions(true)
end
},
inputs: %{
diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs
index 1ccfb34b6d..821c11c17a 100644
--- a/apps/explorer/config/dev.exs
+++ b/apps/explorer/config/dev.exs
@@ -1,7 +1,9 @@
import Config
# Configure your database
-config :explorer, Explorer.Repo, timeout: :timer.seconds(80)
+config :explorer, Explorer.Repo,
+ timeout: :timer.seconds(80),
+ migration_lock: nil
# Configure API database
config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(80)
diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs
index 2d389e59d6..43be5c0e91 100644
--- a/apps/explorer/config/prod.exs
+++ b/apps/explorer/config/prod.exs
@@ -3,7 +3,8 @@ import Config
# Configures the database
config :explorer, Explorer.Repo,
prepare: :unnamed,
- timeout: :timer.seconds(60)
+ timeout: :timer.seconds(60),
+ migration_lock: nil
# Configures API the database
config :explorer, Explorer.Repo.Replica1,
diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs
index 1b3b160791..e9174824ce 100644
--- a/apps/explorer/config/test.exs
+++ b/apps/explorer/config/test.exs
@@ -11,7 +11,8 @@ config :explorer, Explorer.Repo,
# Default of `5_000` was too low for `BlockFetcher` test
ownership_timeout: :timer.minutes(7),
timeout: :timer.seconds(60),
- queue_target: 1000
+ queue_target: 1000,
+ migration_lock: nil
# Configure API database
config :explorer, Explorer.Repo.Replica1,
diff --git a/apps/explorer/lib/explorer/account/tag_transaction.ex b/apps/explorer/lib/explorer/account/tag_transaction.ex
index 26163c92e5..2839b23559 100644
--- a/apps/explorer/lib/explorer/account/tag_transaction.ex
+++ b/apps/explorer/lib/explorer/account/tag_transaction.ex
@@ -153,3 +153,9 @@ defmodule Explorer.Account.TagTransaction do
def get_max_tags_count, do: @max_tag_transaction_per_account
end
+
+defimpl Jason.Encoder, for: Explorer.Account.TagTransaction do
+ def encode(tx_tag, opts) do
+ Jason.Encode.string(tx_tag.name, opts)
+ end
+end
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 3629b1c19b..693ac4b37f 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -5,8 +5,11 @@ defmodule Explorer.Chain do
import Ecto.Query,
only: [
+ dynamic: 1,
+ dynamic: 2,
from: 2,
join: 4,
+ join: 5,
limit: 2,
lock: 2,
offset: 2,
@@ -81,6 +84,23 @@ defmodule Explorer.Chain do
@default_paging_options %PagingOptions{page_size: 50}
+ @token_transfers_per_transaction_preview 10
+ @token_transfers_neccessity_by_association %{
+ [from_address: :smart_contract] => :optional,
+ [to_address: :smart_contract] => :optional,
+ [from_address: :names] => :optional,
+ [to_address: :names] => :optional,
+ token: :required
+ }
+
+ @method_name_to_id_map %{
+ "approve" => "095ea7b3",
+ "transfer" => "a9059cbb",
+ "multicall" => "5ae401dc",
+ "mint" => "40c10f19",
+ "commit" => "f14fcbc8"
+ }
+
@max_incoming_transactions_count 10_000
@revert_msg_prefix_1 "Revert: "
@@ -656,9 +676,7 @@ defmodule Explorer.Chain do
end
def where_block_number_in_period(base_query, from_block, to_block) when is_nil(from_block) and is_nil(to_block) do
- from(q in base_query,
- where: 1
- )
+ base_query
end
def where_block_number_in_period(base_query, from_block, to_block) do
@@ -768,19 +786,40 @@ defmodule Explorer.Chain do
end
end
- @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)
+ 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
- txn_fees =
- Enum.reduce(transactions, Decimal.new(0), fn %{gas_used: gas_used, gas_price: gas_price}, acc ->
+ 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.mult(Decimal.new(gas_price))
|> 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(
@@ -790,17 +829,9 @@ defmodule Explorer.Chain do
)
) || %Wei{value: Decimal.new(0)}
- burned_fee_counter =
- transactions
- |> Enum.reduce(Decimal.new(0), fn %{gas_used: gas_used}, acc ->
- gas_used
- |> Decimal.new()
- |> Decimal.add(acc)
- end)
-
has_uncles? = is_list(block.uncles) and not Enum.empty?(block.uncles)
- burned_fees = base_fee_per_gas && Wei.mult(%Wei{value: Decimal.new(base_fee_per_gas)}, burned_fee_counter)
+ burned_fees = burned_fees(transactions, base_fee_per_gas)
uncle_reward = (has_uncles? && Wei.mult(static_reward, Decimal.from_float(@uncle_reward_coef))) || nil
%{
@@ -861,8 +892,10 @@ defmodule Explorer.Chain do
`: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]) :: [Transaction.t()]
- def block_to_transactions(block_hash, options \\ []) when is_list(options) do
+ @spec block_to_transactions(Hash.Full.t(), [paging_options | necessity_by_association_option], 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
@@ -871,8 +904,12 @@ defmodule Explorer.Chain do
|> join(:inner, [transaction], block in assoc(transaction, :block))
|> where([_, block], block.hash == ^block_hash)
|> join_associations(necessity_by_association)
- |> preload([{:token_transfers, [:token, :from_address, :to_address]}])
+ |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|> Repo.all()
+ |> (&if(old_ui?,
+ do: &1,
+ else: Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_neccessity_by_association) end)
+ )).()
end
@doc """
@@ -1031,8 +1068,8 @@ defmodule Explorer.Chain do
iex> Explorer.Chain.confirmations(block, block_height: 0)
{:ok, 1}
"""
- @spec confirmations(Block.t(), [{:block_height, block_height()}]) ::
- {:ok, non_neg_integer()} | {:error, :non_consensus}
+ @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)
@@ -1042,6 +1079,8 @@ defmodule Explorer.Chain do
def confirmations(%Block{consensus: false}, _), do: {:error, :non_consensus}
+ def confirmations(nil, _), do: {:error, :pending}
+
@doc """
Creates an address.
@@ -2019,6 +2058,41 @@ defmodule Explorer.Chain do
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 dont'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,
+ preload_to_detect_tt? \\ true
+ ) do
+ 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(^if(preload_to_detect_tt?, do: 1, else: @token_transfers_per_transaction_preview + 1))
+ |> order_by([token_transfer], asc: token_transfer.log_index)
+ |> join_associations(necessity_by_association)
+ |> Repo.all()
+
+ %Transaction{transaction | token_transfers: token_transfers}
+ end
+
+ def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
+
+ defp debug(value, key) do
+ require Logger
+ Logger.configure(truncate: :infinity)
+ Logger.info(key)
+ Logger.info(Kernel.inspect(value, limit: :infinity, printable_limit: :infinity))
+ value
+ end
+
@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`.
@@ -2504,6 +2578,15 @@ defmodule Explorer.Chain do
@doc """
Return the balance in usd corresponding to this token. Return nil if the usd_value of the token is not present.
"""
+ def balance_in_usd(_token_balance, %{usd_value: nil}) do
+ nil
+ end
+
+ def balance_in_usd(token_balance, %{usd_value: usd_value, decimals: decimals}) do
+ tokens = CurrencyHelpers.divide_decimals(token_balance.value, decimals)
+ Decimal.mult(tokens, usd_value)
+ end
+
def balance_in_usd(%{token: %{usd_value: nil}}) do
nil
end
@@ -3171,7 +3254,7 @@ defmodule Explorer.Chain do
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(paging_options: paging_options)
+ 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
@@ -3187,26 +3270,17 @@ defmodule Explorer.Chain do
the `block_number` and `index` that are passed.
"""
- @spec recent_collated_transactions([paging_options | necessity_by_association_option]) :: [Transaction.t()]
- def recent_collated_transactions(options \\ []) when is_list(options) do
+ @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option], [String.t()], [
+ :atom
+ ]) :: [
+ Transaction.t()
+ ]
+ def recent_collated_transactions(old_ui?, options \\ [], method_id_filter \\ [], type_filter \\ [])
+ when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
- if is_nil(paging_options.key) do
- paging_options.page_size
- |> Transactions.take_enough()
- |> case do
- nil ->
- transactions = fetch_recent_collated_transactions(paging_options, necessity_by_association)
- Transactions.update(transactions)
- transactions
-
- transactions ->
- transactions
- end
- else
- fetch_recent_collated_transactions(paging_options, necessity_by_association)
- end
+ fetch_recent_collated_transactions(old_ui?, paging_options, necessity_by_association, method_id_filter, type_filter)
end
# RAP - random access pagination
@@ -3264,13 +3338,26 @@ defmodule Explorer.Chain do
|> Repo.aggregate(:count, :hash)
end
- def fetch_recent_collated_transactions(paging_options, necessity_by_association) do
+ def fetch_recent_collated_transactions(
+ old_ui?,
+ paging_options,
+ necessity_by_association,
+ method_id_filter,
+ type_filter
+ ) 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)
- |> preload([{:token_transfers, [:token, :from_address, :to_address]}])
+ |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
+ |> debug("result collated query")
|> Repo.all()
+ |> (&if(old_ui?,
+ do: &1,
+ else: Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_neccessity_by_association) end)
+ )).()
end
@doc """
@@ -3297,8 +3384,11 @@ defmodule Explorer.Chain do
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]) :: [Transaction.t()]
- def recent_pending_transactions(options \\ []) when is_list(options) do
+ @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false, [String.t()], [
+ :atom
+ ]) :: [Transaction.t()]
+ def recent_pending_transactions(options \\ [], old_ui? \\ true, method_id_filter \\ [], type_filter \\ [])
+ when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
@@ -3306,10 +3396,17 @@ defmodule Explorer.Chain do
|> 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, desc: transaction.hash)
|> join_associations(necessity_by_association)
- |> preload([{:token_transfers, [:token, :from_address, :to_address]}])
+ |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
+ |> debug("result pendging query")
|> Repo.all()
+ |> (&if(old_ui?,
+ do: &1,
+ else: Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_neccessity_by_association) end)
+ )).()
end
def pending_transactions_query(query) do
@@ -4334,6 +4431,16 @@ defmodule Explorer.Chain do
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)
@@ -5040,8 +5147,8 @@ defmodule Explorer.Chain do
Repo.one(query)
end
- @spec address_to_balances_by_day(Hash.Address.t()) :: [balance_by_day]
- def address_to_balances_by_day(address_hash) do
+ @spec address_to_balances_by_day(Hash.Address.t(), true | false) :: [balance_by_day]
+ def address_to_balances_by_day(address_hash, api? \\ false) do
latest_block_timestamp =
address_hash
|> CoinBalance.last_coin_balance_timestamp()
@@ -5052,7 +5159,7 @@ defmodule Explorer.Chain do
|> Repo.all()
|> Enum.sort_by(fn %{date: d} -> {d.year, d.month, d.day} end)
|> replace_last_value(latest_block_timestamp)
- |> normalize_balances_by_day()
+ |> normalize_balances_by_day(api?)
end
# https://github.com/blockscout/blockscout/issues/2658
@@ -5062,12 +5169,12 @@ defmodule Explorer.Chain do
defp replace_last_value(items, _), do: items
- defp normalize_balances_by_day(balances_by_day) do
+ defp normalize_balances_by_day(balances_by_day, api?) do
result =
balances_by_day
|> Enum.filter(fn day -> day.value end)
- |> Enum.map(fn day -> Map.update!(day, :date, &to_string(&1)) end)
- |> Enum.map(fn day -> Map.update!(day, :value, &Wei.to(&1, :ether)) 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())
@@ -6241,4 +6348,127 @@ defmodule Explorer.Chain do
|> to_string()
|> String.downcase()
end
+
+ def recent_transactions(options, [:pending | _], method_id_filter, type_filter_options) do
+ recent_pending_transactions(options, false, method_id_filter, type_filter_options)
+ end
+
+ def recent_transactions(options, _, method_id_filter, type_filter_options) do
+ recent_collated_transactions(false, options, method_id_filter, type_filter_options)
+ end
+
+ 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 (is_nil(tx.to_address_hash) and not is_nil(created_token)))
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex
index 03513580ba..4cf3fbd813 100644
--- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex
+++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex
@@ -165,7 +165,6 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
where: ctb.value > 0,
left_join: t in Token,
on: ctb.token_contract_address_hash == t.contract_address_hash,
- preload: :token,
select: {ctb, t},
order_by: [desc: ctb.value, asc: t.type, asc: t.name]
)
diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex
index 88696c91c3..be55eba91d 100644
--- a/apps/explorer/lib/explorer/chain/transaction.ex
+++ b/apps/explorer/lib/explorer/chain/transaction.ex
@@ -9,6 +9,7 @@ defmodule Explorer.Chain.Transaction do
alias ABI.FunctionSelector
+ alias Ecto.Association.NotLoaded
alias Ecto.Changeset
alias Explorer.{Chain, Repo}
@@ -464,6 +465,18 @@ defmodule Explorer.Chain.Transaction do
def decoded_input_data(%__MODULE__{input: %{bytes: bytes}}) when bytes in [nil, <<>>], do: {:error, :no_input_data}
def decoded_input_data(%__MODULE__{to_address: %{contract_code: nil}}), do: {:error, :not_a_contract_call}
+ def decoded_input_data(%__MODULE__{
+ to_address: %{smart_contract: %NotLoaded{}},
+ input: input,
+ hash: hash
+ }) do
+ decoded_input_data(%__MODULE__{
+ to_address: %{smart_contract: nil},
+ input: input,
+ hash: hash
+ })
+ end
+
def decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil},
input: %{bytes: <> = data},
@@ -499,7 +512,7 @@ defmodule Explorer.Chain.Transaction do
hash: hash
}) do
case do_decoded_input_data(data, abi, address_hash, hash) do
- # In some cases transactions use methods of some unpredictadle contracts, so we can try to look up for method in a whole DB
+ # In some cases transactions use methods of some unpredictable contracts, so we can try to look up for method in a whole DB
{:error, :could_not_decode} ->
case decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil},
@@ -558,14 +571,16 @@ defmodule Explorer.Chain.Transaction do
def get_method_name(_), do: "Transfer"
- defp parse_method_name(method_desc) do
+ def parse_method_name(method_desc, need_upcase \\ true) do
method_desc
|> String.split("(")
|> Enum.at(0)
- |> upcase_first
+ |> upcase_first(need_upcase)
end
- defp upcase_first(<>), do: String.upcase(<>) <> rest
+ defp upcase_first(string, false), do: string
+
+ defp upcase_first(<>, true), do: String.upcase(<>) <> rest
defp function_call(name, mapping) do
text =
diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex
index 8fa3a69b22..533174ba45 100644
--- a/apps/explorer/lib/explorer/chain/wei.ex
+++ b/apps/explorer/lib/explorer/chain/wei.ex
@@ -268,7 +268,8 @@ defimpl Inspect, for: Explorer.Chain.Wei do
end
defimpl Jason.Encoder, for: Explorer.Chain.Wei do
- def encode(wei, _) do
- Decimal.to_string(wei.value)
+ def encode(wei, opts) do
+ # changed since it's needed to return wei value (which is big number) as string
+ Jason.Encode.struct(wei.value, opts)
end
end
diff --git a/apps/explorer/lib/explorer/counters/address_tokens_usd_sum.ex b/apps/explorer/lib/explorer/counters/address_tokens_usd_sum.ex
index d3f74204a3..12dfb1e0cb 100644
--- a/apps/explorer/lib/explorer/counters/address_tokens_usd_sum.ex
+++ b/apps/explorer/lib/explorer/counters/address_tokens_usd_sum.ex
@@ -53,9 +53,9 @@ defmodule Explorer.Counters.AddressTokenUsdSum do
@spec address_tokens_usd_sum([{Address.CurrentTokenBalance, Explorer.Chain.Token}]) :: Decimal.t()
defp address_tokens_usd_sum(token_balances) do
token_balances
- |> Enum.reduce(Decimal.new(0), fn {token_balance, _}, acc ->
- if token_balance.value && token_balance.token.usd_value do
- Decimal.add(acc, Chain.balance_in_usd(token_balance))
+ |> Enum.reduce(Decimal.new(0), fn {token_balance, token}, acc ->
+ if token_balance.value && token.usd_value do
+ Decimal.add(acc, Chain.balance_in_usd(token_balance, token))
else
acc
end
diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex
index 01a815cf8c..150adb1a73 100644
--- a/apps/explorer/lib/explorer/market/market.ex
+++ b/apps/explorer/lib/explorer/market/market.ex
@@ -76,7 +76,7 @@ defmodule Explorer.Market do
Enum.map(tokens, fn item ->
case item do
{token_balance, token} ->
- {add_price(token_balance), token}
+ {token_balance, add_price(token)}
token_balance ->
add_price(token_balance)
diff --git a/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs b/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs
new file mode 100644
index 0000000000..24244fd249
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs
@@ -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
diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs
index c5d5f0a4fd..a98e31597a 100644
--- a/apps/explorer/test/explorer/chain_test.exs
+++ b/apps/explorer/test/explorer/chain_test.exs
@@ -3829,12 +3829,12 @@ defmodule Explorer.ChainTest do
describe "recent_collated_transactions/1" do
test "with no collated transactions it returns an empty list" do
- assert [] == Explorer.Chain.recent_collated_transactions()
+ assert [] == Explorer.Chain.recent_collated_transactions(true)
end
test "it excludes pending transactions" do
insert(:transaction)
- assert [] == Explorer.Chain.recent_collated_transactions()
+ assert [] == Explorer.Chain.recent_collated_transactions(true)
end
test "returns a list of recent collated transactions" do
@@ -3846,7 +3846,7 @@ defmodule Explorer.ChainTest do
oldest_seen = Enum.at(newest_first_transactions, 9)
paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}}
- recent_collated_transactions = Explorer.Chain.recent_collated_transactions(paging_options: paging_options)
+ recent_collated_transactions = Explorer.Chain.recent_collated_transactions(true, paging_options: paging_options)
assert length(recent_collated_transactions) == 10
assert hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash
@@ -3868,10 +3868,11 @@ defmodule Explorer.ChainTest do
to_address: address,
transaction: transaction,
token_contract_address: token_contract_address,
- token: token
+ token: token,
+ block: transaction.block
)
- fetched_transaction = List.first(Explorer.Chain.recent_collated_transactions())
+ fetched_transaction = List.first(Explorer.Chain.recent_collated_transactions(true))
assert fetched_transaction.hash == transaction.hash
assert length(fetched_transaction.token_transfers) == 2
end
diff --git a/config/runtime.exs b/config/runtime.exs
index a8a17e2e57..f649937b3d 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -170,6 +170,8 @@ config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance,
# days
coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10")
+config :block_scout_web, BlockScoutWeb.API.V2, enabled: System.get_env("API_V2_ENABLED") == "true"
+
########################
### Ethereum JSONRPC ###
########################