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 %>
@@ -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 ### ########################