diff --git a/apps/block_scout_web/.sobelow-conf b/apps/block_scout_web/.sobelow-conf index 1604d1f66d..70a8e7b010 100644 --- a/apps/block_scout_web/.sobelow-conf +++ b/apps/block_scout_web/.sobelow-conf @@ -7,7 +7,8 @@ format: "compact", ignore: ["Config.Headers", "Config.CSWH", "XSS.SendResp", "XSS.Raw"], ignore_files: [ - "apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex", - "apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex" + "apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex", + "apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex", + "apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex" ] ] diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 40ff4eb8f4..a1dd4491c3 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -87,11 +87,11 @@ config :prometheus, BlockScoutWeb.Prometheus.PhoenixInstrumenter, config :spandex_phoenix, tracer: BlockScoutWeb.Tracer -config :block_scout_web, BlockScoutWeb.ApiRouter, +config :block_scout_web, BlockScoutWeb.Routers.ApiRouter, writing_enabled: !ConfigHelper.parse_bool_env_var("API_V1_WRITE_METHODS_DISABLED"), reading_enabled: !ConfigHelper.parse_bool_env_var("API_V1_READ_METHODS_DISABLED") -config :block_scout_web, BlockScoutWeb.WebRouter, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_WEBAPP") +config :block_scout_web, BlockScoutWeb.Routers.WebRouter, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_WEBAPP") config :block_scout_web, BlockScoutWeb.CSPHeader, mixpanel_url: System.get_env("MIXPANEL_URL", "https://api-js.mixpanel.com"), diff --git a/apps/block_scout_web/lib/block_scout_web.ex b/apps/block_scout_web/lib/block_scout_web.ex index 4df825cff2..7cdc6b3633 100644 --- a/apps/block_scout_web/lib/block_scout_web.ex +++ b/apps/block_scout_web/lib/block_scout_web.ex @@ -24,13 +24,13 @@ defmodule BlockScoutWeb do import BlockScoutWeb.Controller import BlockScoutWeb.Router.Helpers - import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2] + import BlockScoutWeb.Routers.WebRouter.Helpers, except: [static_path: 2] import BlockScoutWeb.Gettext import BlockScoutWeb.ErrorHelper import BlockScoutWeb.Routers.AccountRouter.Helpers, except: [static_path: 2] import Plug.Conn - alias BlockScoutWeb.AdminRouter.Helpers, as: AdminRoutes + alias BlockScoutWeb.Routers.AdminRouter.Helpers, as: AdminRoutes end end @@ -61,7 +61,7 @@ defmodule BlockScoutWeb do import Explorer.Chain.CurrencyHelper, only: [divide_decimals: 2] - import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2] + import BlockScoutWeb.Routers.WebRouter.Helpers, except: [static_path: 2] end end diff --git a/apps/block_scout_web/lib/block_scout_web/channels/token_instance_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/token_instance_channel.ex new file mode 100644 index 0000000000..5a556eb874 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/channels/token_instance_channel.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.TokenInstanceChannel do + @moduledoc """ + Establishes pub/sub channel for live updates of token instances events. + """ + use BlockScoutWeb, :channel + + intercept(["fetched_token_instance_metadata"]) + + def join("fetched_token_instance_metadata", _params, socket) do + {:ok, %{}, socket} + end + + def join("token_instances:" <> _token_contract_address_hash, _params, socket) do + {:ok, %{}, socket} + end + + def handle_out( + "fetched_token_instance_metadata", + res, + %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket + ) do + push(socket, "fetched_token_instance_metadata", res) + + {:noreply, socket} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex b/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex index 7f1b4993a1..5d51597e35 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.UserSocket do channel("rewards:*", BlockScoutWeb.RewardChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("tokens:*", BlockScoutWeb.TokenChannel) + channel("token_instances:*", BlockScoutWeb.TokenInstanceChannel) def connect(%{"locale" => locale}, socket) do {:ok, assign(socket, :locale, 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 index 740b716dc3..8ac5295d60 100644 --- 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 @@ -11,6 +11,7 @@ defmodule BlockScoutWeb.UserSocketV2 do channel("rewards:*", BlockScoutWeb.RewardChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("tokens:*", BlockScoutWeb.TokenChannel) + channel("token_instances:*", BlockScoutWeb.TokenInstanceChannel) channel("zkevm_batches:*", BlockScoutWeb.PolygonZkevmConfirmedBatchChannel) def connect(_params, socket) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/admin/setup_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/admin/setup_controller.ex index 9005fe3587..1d0f82f4d6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/admin/setup_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/admin/setup_controller.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.Admin.SetupController do use BlockScoutWeb, :controller - import BlockScoutWeb.AdminRouter.Helpers + import BlockScoutWeb.Routers.AdminRouter.Helpers alias BlockScoutWeb.Endpoint alias Explorer.Accounts.User.Registration diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index 0263abd96d..17fd203f6c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -110,7 +110,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do end defp action_accessed?(action, write_actions) do - conf = Application.get_env(:block_scout_web, BlockScoutWeb.ApiRouter) + conf = Application.get_env(:block_scout_web, BlockScoutWeb.Routers.ApiRouter) if action in write_actions do conf[:writing_enabled] || {:error, :no_action} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex index 1c2c384403..d8ef80f8f8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex @@ -20,11 +20,11 @@ defmodule BlockScoutWeb.API.V2.CSVExportController do def export_token_holders(conn, %{"address_hash_param" => address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, {:recaptcha, true} <- {:recaptcha, Application.get_env(:block_scout_web, :recaptcha)[:is_disabled] || - CSVHelper.captcha_helper().recaptcha_passed?(params["recaptcha_response"])} do + CSVHelper.captcha_helper().recaptcha_passed?(params["recaptcha_response"])}, + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do token_holders = Chain.fetch_token_holders_from_token_hash_for_csv(address_hash, @options) token_holders 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 index eb6a7447e1..19448ad5fa 100644 --- 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 @@ -7,7 +7,6 @@ defmodule BlockScoutWeb.API.V2.FallbackController do alias BlockScoutWeb.API.V2.ApiView alias Ecto.Changeset - @verification_failed "API v2 smart-contract verification failed" @invalid_parameters "Invalid parameter(s)" @invalid_address_hash "Invalid address hash" @invalid_hash "Invalid hash" @@ -36,7 +35,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:format, _params}) do Logger.error(fn -> - ["#{@verification_failed}: #{@invalid_parameters}"] + ["#{@invalid_parameters}"] end) conn @@ -47,7 +46,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:format_address, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@invalid_address_hash}"] + ["#{@invalid_address_hash}"] end) conn @@ -58,7 +57,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:format_url, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@invalid_url}"] + ["#{@invalid_url}"] end) conn @@ -69,7 +68,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:not_found, _, :empty_items_with_next_page_params}) do Logger.error(fn -> - ["#{@verification_failed}: :empty_items_with_next_page_params"] + [":empty_items_with_next_page_params"] end) conn @@ -78,7 +77,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:not_found, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@not_found}"] + ["#{@not_found}"] end) conn @@ -89,7 +88,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:contract_interaction_disabled, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@contract_interaction_disabled}"] + ["#{@contract_interaction_disabled}"] end) conn @@ -100,7 +99,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:error, {:invalid, :hash}}) do Logger.error(fn -> - ["#{@verification_failed}: #{@invalid_hash}"] + ["#{@invalid_hash}"] end) conn @@ -111,7 +110,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:error, {:invalid, :number}}) do Logger.error(fn -> - ["#{@verification_failed}: #{@invalid_number}"] + ["#{@invalid_number}"] end) conn @@ -122,7 +121,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:error, :not_found}) do Logger.error(fn -> - ["#{@verification_failed}: :not_found"] + [":not_found"] end) conn @@ -138,7 +137,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:restricted_access, true}) do Logger.error(fn -> - ["#{@verification_failed}: #{@restricted_access}"] + ["#{@restricted_access}"] end) conn @@ -149,7 +148,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:already_verified, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@already_verified}"] + ["#{@already_verified}"] end) conn @@ -159,7 +158,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:no_json_file, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@json_not_found}"] + ["#{@json_not_found}"] end) conn @@ -169,7 +168,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:file_error, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@error_while_reading_json}"] + ["#{@error_while_reading_json}"] end) conn @@ -179,7 +178,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:libs_format, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@error_in_libraries}"] + ["#{@error_in_libraries}"] end) conn @@ -189,7 +188,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:lost_consensus, {:ok, block}}) do Logger.error(fn -> - ["#{@verification_failed}: #{@block_lost_consensus}"] + ["#{@block_lost_consensus}"] end) conn @@ -199,7 +198,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:lost_consensus, {:error, :not_found}}) do Logger.error(fn -> - ["#{@verification_failed}: #{@block_lost_consensus}"] + ["#{@block_lost_consensus}"] end) conn @@ -208,7 +207,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:recaptcha, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@invalid_captcha_resp}"] + ["#{@invalid_captcha_resp}"] end) conn @@ -219,7 +218,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:auth, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@unauthorized}"] + ["#{@unauthorized}"] end) conn @@ -230,7 +229,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:sensitive_endpoints_api_key, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@not_configured_api_key}"] + ["#{@not_configured_api_key}"] end) conn @@ -241,7 +240,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do def call(conn, {:api_key, _}) do Logger.error(fn -> - ["#{@verification_failed}: #{@wrong_api_key}"] + ["#{@wrong_api_key}"] end) conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index fbe4bbf998..898f2d8624 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -6,6 +6,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do alias BlockScoutWeb.API.V2.{AddressView, TransactionView} alias Explorer.{Chain, Helper, Repo} alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} + alias Explorer.Chain.CSVExport.Helper, as: CSVHelper + alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand alias Indexer.Fetcher.OnDemand.TokenTotalSupply, as: TokenTotalSupplyOnDemand import BlockScoutWeb.Chain, @@ -185,29 +187,13 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_str} = params) do + def instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, - {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do - token_instance = - case Chain.nft_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do - {:ok, token_instance} -> - token_instance - |> Chain.select_repo(@api_true).preload(:owner) - |> Chain.put_owner_to_token_instance(token, @api_true) - - {:error, :not_found} -> - %Instance{ - token_id: Decimal.new(token_id), - metadata: nil, - owner: nil, - token_contract_address_hash: address_hash - } - |> Instance.put_is_unique(token, @api_true) - |> Chain.put_owner_to_token_instance(token, @api_true) - end + {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do + token_instance = token_instance_from_token_id_and_token_address(token_id, address_hash, token) conn |> put_status(200) @@ -218,12 +204,15 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def transfers_by_instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_str} = params) do + def transfers_by_instance( + conn, + %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params + ) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, - {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do + {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do paging_options = paging_options(params) results = @@ -248,12 +237,12 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def holders_by_instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_str} = params) do + def holders_by_instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, - {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do + {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do paging_options = paging_options(params) results = @@ -281,13 +270,13 @@ defmodule BlockScoutWeb.API.V2.TokenController do def transfers_count_by_instance( conn, - %{"address_hash_param" => address_hash_string, "token_id" => token_id_str} = params + %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params ) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, - {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do + {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do conn |> put_status(200) |> json(%{ @@ -340,6 +329,61 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> render(:bridged_tokens, %{tokens: tokens, next_page_params: next_page_params}) end + def refetch_metadata( + conn, + params + ) do + address_hash_string = params["address_hash_param"] + token_id_string = params["token_id"] + recaptcha_response = params["recaptcha_response"] + + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:recaptcha, true} <- {:recaptcha, CSVHelper.captcha_helper().recaptcha_passed?(recaptcha_response)}, + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)}, + {:ok, token_instance} <- Chain.nft_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do + token_instance_with_token = + token_instance + |> put_token_to_instance(token) + + TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance_with_token) + + conn + |> put_status(200) + |> json(%{message: "OK"}) + end + end + defp put_owner(token_instances, holder_address), do: Enum.map(token_instances, fn token_instance -> %Instance{token_instance | owner: holder_address} end) + + defp token_instance_from_token_id_and_token_address(token_id, address_hash, token) do + case Chain.nft_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do + {:ok, token_instance} -> + token_instance + |> Chain.select_repo(@api_true).preload([:owner]) + |> Chain.put_owner_to_token_instance(token, @api_true) + + {:error, :not_found} -> + %Instance{ + token_id: Decimal.new(token_id), + metadata: nil, + owner: nil, + token: nil, + token_contract_address_hash: address_hash + } + |> Instance.put_is_unique(token, @api_true) + |> Chain.put_owner_to_token_instance(token, @api_true) + end + end + + @spec put_token_to_instance(Instance.t(), Token.t()) :: Instance.t() + defp put_token_to_instance( + token_instance, + token + ) do + %{token_instance | token: token} + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex index 2b2eab99b7..4f227cb3f1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex @@ -9,11 +9,11 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3] - def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str, "type" => "JSON"} = params) do + def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(address_hash), false <- Chain.erc_20_token?(token), - {token_id, ""} <- Integer.parse(token_id_str), + {token_id, ""} <- Integer.parse(token_id_string), token_holders <- Chain.fetch_token_holders_from_token_hash_and_token_id(address_hash, token_id, paging_options(params)) do {token_holders_paginated, next_page} = split_list_by_page(token_holders) @@ -53,13 +53,13 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do end end - def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str}) do + def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string}) do options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), false <- Chain.erc_20_token?(token), - {token_id, ""} <- Integer.parse(token_id_str) do + {token_id, ""} <- Integer.parse(token_id_string) do case Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do {:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token) {:error, :not_found} -> Helper.render(conn, nil, hash, token_id, token) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex index 0036a95563..84dd18da53 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex @@ -4,13 +4,13 @@ defmodule BlockScoutWeb.Tokens.Instance.MetadataController do alias BlockScoutWeb.Tokens.Instance.Helper alias Explorer.Chain - def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str}) do + def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string}) do options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), false <- Chain.erc_20_token?(token), - {token_id, ""} <- Integer.parse(token_id_str), + {token_id, ""} <- Integer.parse(token_id_string), {:ok, token_instance} <- Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do if token_instance.metadata do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex index 30a8212a75..62c9c46386 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex @@ -13,11 +13,11 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) @burn_address_hash burn_address_hash - def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str, "type" => "JSON"} = params) do + def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string, "type" => "JSON"} = params) do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash), false <- Chain.erc_20_token?(token), - {token_id, ""} <- Integer.parse(token_id_str), + {token_id, ""} <- Integer.parse(token_id_string), token_transfers <- Chain.fetch_token_transfers_from_token_hash_and_token_id(hash, token_id, paging_options(params)) do {token_transfers_paginated, next_page} = split_list_by_page(token_transfers) @@ -56,13 +56,13 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do end end - def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str}) do + def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string}) do options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), false <- Chain.erc_20_token?(token), - {token_id, ""} <- Integer.parse(token_id_str) do + {token_id, ""} <- Integer.parse(token_id_string) do case Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do {:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token) {:error, :not_found} -> Helper.render(conn, nil, hash, token_id, token) 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 9d5ecfda3a..951579c042 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -243,6 +243,17 @@ defmodule BlockScoutWeb.Notifier do Endpoint.broadcast("addresses:#{to_string(address_hash)}", "fetched_bytecode", %{fetched_bytecode: fetched_bytecode}) end + def handle_event( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [token_contract_address_hash_string, token_id, fetched_token_instance_metadata]} + ) do + Endpoint.broadcast( + "token_instances:#{token_contract_address_hash_string}", + "fetched_token_instance_metadata", + %{token_id: token_id, fetched_metadata: fetched_token_instance_metadata} + ) + end + def handle_event({:chain_event, :changed_bytecode, :on_demand, [address_hash]}) do Endpoint.broadcast("addresses:#{to_string(address_hash)}", "changed_bytecode", %{}) end diff --git a/apps/block_scout_web/lib/block_scout_web/plug/admin/check_owner_registered.ex b/apps/block_scout_web/lib/block_scout_web/plug/admin/check_owner_registered.ex index b15fd168f5..b1ff805326 100644 --- a/apps/block_scout_web/lib/block_scout_web/plug/admin/check_owner_registered.ex +++ b/apps/block_scout_web/lib/block_scout_web/plug/admin/check_owner_registered.ex @@ -9,7 +9,7 @@ defmodule BlockScoutWeb.Plug.Admin.CheckOwnerRegistered do import Phoenix.Controller, only: [redirect: 2] import Plug.Conn - alias BlockScoutWeb.AdminRouter.Helpers, as: AdminRoutes + alias BlockScoutWeb.Routers.AdminRouter.Helpers, as: AdminRoutes alias Explorer.Admin alias Plug.Conn diff --git a/apps/block_scout_web/lib/block_scout_web/plug/admin/require_admin_role.ex b/apps/block_scout_web/lib/block_scout_web/plug/admin/require_admin_role.ex index bd11cd5509..2a70d8a0e0 100644 --- a/apps/block_scout_web/lib/block_scout_web/plug/admin/require_admin_role.ex +++ b/apps/block_scout_web/lib/block_scout_web/plug/admin/require_admin_role.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Plug.Admin.RequireAdminRole do import Phoenix.Controller, only: [redirect: 2] - alias BlockScoutWeb.AdminRouter.Helpers, as: AdminRoutes + alias BlockScoutWeb.Routers.AdminRouter.Helpers, as: AdminRoutes alias Explorer.Admin def init(opts), do: opts diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index dc956b249f..b19ead1cc0 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -28,6 +28,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do Subscriber.to(:token_total_supply, :on_demand) Subscriber.to(:changed_bytecode, :on_demand) Subscriber.to(:fetched_bytecode, :on_demand) + Subscriber.to(:fetched_token_instance_metadata, :on_demand) Subscriber.to(:eth_bytecode_db_lookup_started, :on_demand) Subscriber.to(:zkevm_confirmed_batches, :realtime) # Does not come from the indexer diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index b64a0d1c7d..07300fd79b 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -2,13 +2,12 @@ defmodule BlockScoutWeb.Router do use BlockScoutWeb, :router alias BlockScoutWeb.Plug.{GraphQL, RateLimit} - alias BlockScoutWeb.{ApiRouter, WebRouter} - alias BlockScoutWeb.Routers.AccountRouter + alias BlockScoutWeb.Routers.{AccountRouter, ApiRouter, WebRouter} @max_query_string_length 5_000 if Application.compile_env(:block_scout_web, :admin_panel_enabled) do - forward("/admin", BlockScoutWeb.AdminRouter) + forward("/admin", BlockScoutWeb.Routers.AdminRouter) end pipeline :browser do @@ -96,6 +95,6 @@ defmodule BlockScoutWeb.Router do end if Application.compile_env(:block_scout_web, WebRouter)[:enabled] do - forward("/", BlockScoutWeb.WebRouter) + forward("/", BlockScoutWeb.Routers.WebRouter) end end diff --git a/apps/block_scout_web/lib/block_scout_web/admin_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex similarity index 96% rename from apps/block_scout_web/lib/block_scout_web/admin_router.ex rename to apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex index 213d4abff4..d452980ae9 100644 --- a/apps/block_scout_web/lib/block_scout_web/admin_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex @@ -1,4 +1,4 @@ -defmodule BlockScoutWeb.AdminRouter do +defmodule BlockScoutWeb.Routers.AdminRouter do @moduledoc """ Router for admin pages. """ diff --git a/apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_key_v2_router.ex similarity index 92% rename from apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex rename to apps/block_scout_web/lib/block_scout_web/routers/api_key_v2_router.ex index 29b6fe114f..8c656e6ced 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_key_v2_router.ex @@ -1,4 +1,4 @@ -defmodule BlockScoutWeb.APIKeyV2Router do +defmodule BlockScoutWeb.Routers.APIKeyV2Router do @moduledoc """ Router for /api/v2/key. This route has separate router in order to avoid rate limiting """ diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex similarity index 93% rename from apps/block_scout_web/lib/block_scout_web/api_router.ex rename to apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index e58618a34c..a2f6bd9d4b 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -8,18 +8,21 @@ defmodule RPCTranslatorForwarder do defdelegate call(conn, opts), to: RPCTranslator end -defmodule BlockScoutWeb.ApiRouter do +defmodule BlockScoutWeb.Routers.ApiRouter do @moduledoc """ Router for API """ use BlockScoutWeb, :router - alias BlockScoutWeb.{AddressTransactionController, APIKeyV2Router, SmartContractsApiV2Router, UtilsApiV2Router} + alias BlockScoutWeb.AddressTransactionController + alias BlockScoutWeb.Routers.{APIKeyV2Router, SmartContractsApiV2Router, TokensApiV2Router, UtilsApiV2Router} alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} alias BlockScoutWeb.Routers.AccountRouter @max_query_string_length 5_000 forward("/v2/smart-contracts", SmartContractsApiV2Router) + forward("/v2/tokens", TokensApiV2Router) + forward("/v2/key", APIKeyV2Router) forward("/v2/utils", UtilsApiV2Router) @@ -171,24 +174,6 @@ defmodule BlockScoutWeb.ApiRouter do get("/:address_hash_param/nft/collections", V2.AddressController, :nft_collections) end - scope "/tokens" do - if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do - get("/bridged", V2.TokenController, :bridged_tokens_list) - end - - get("/", V2.TokenController, :tokens_list) - get("/:address_hash_param", V2.TokenController, :token) - get("/:address_hash_param/counters", V2.TokenController, :counters) - get("/:address_hash_param/transfers", V2.TokenController, :transfers) - get("/:address_hash_param/holders", V2.TokenController, :holders) - get("/:address_hash_param/holders/csv", V2.CSVExportController, :export_token_holders) - get("/:address_hash_param/instances", V2.TokenController, :instances) - get("/:address_hash_param/instances/:token_id", V2.TokenController, :instance) - get("/:address_hash_param/instances/:token_id/transfers", V2.TokenController, :transfers_by_instance) - get("/:address_hash_param/instances/:token_id/holders", V2.TokenController, :holders_by_instance) - get("/:address_hash_param/instances/:token_id/transfers-count", V2.TokenController, :transfers_count_by_instance) - end - scope "/main-page" do get("/blocks", V2.MainPageController, :blocks) get("/transactions", V2.MainPageController, :transactions) diff --git a/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex similarity index 59% rename from apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex rename to apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex index 86ef4f49ff..ab1f1d4d89 100644 --- a/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex @@ -1,11 +1,31 @@ # This file in ignore list of `sobelow`, be careful while adding new endpoints here -defmodule BlockScoutWeb.SmartContractsApiV2Router do +defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do @moduledoc """ Router for /api/v2/smart-contracts. This route has separate router in order to ignore sobelow's warning about missing CSRF protection """ use BlockScoutWeb, :router + alias BlockScoutWeb.API.V2 alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + @max_query_string_length 5_000 + + pipeline :api_v2 do + plug( + Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + query_string_length: @max_query_string_length, + pass: ["*/*"], + json_decoder: Poison + ) + + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) + plug(:accepts, ["json"]) + plug(CheckApiV2) + plug(:fetch_session) + plug(:protect_from_forgery) + plug(RateLimit) + end + pipeline :api_v2_no_forgery_protect do plug( Plug.Parsers, @@ -24,9 +44,7 @@ defmodule BlockScoutWeb.SmartContractsApiV2Router do end scope "/", as: :api_v2 do - pipe_through(:api_v2_no_forgery_protect) - - alias BlockScoutWeb.API.V2 + pipe_through(:api_v2) get("/", V2.SmartContractController, :smart_contracts_list) get("/counters", V2.SmartContractController, :smart_contracts_counters) @@ -41,15 +59,17 @@ defmodule BlockScoutWeb.SmartContractsApiV2Router do get("/:address_hash/audit-reports", V2.SmartContractController, :audit_reports_list) get("/verification/config", V2.VerificationController, :config) + end + + scope "/:address_hash/verification/via", as: :api_v2 do + pipe_through(:api_v2_no_forgery_protect) - scope "/:address_hash/verification/via" do - post("/flattened-code", V2.VerificationController, :verification_via_flattened_code) - post("/standard-input", V2.VerificationController, :verification_via_standard_input) - post("/sourcify", V2.VerificationController, :verification_via_sourcify) - post("/multi-part", V2.VerificationController, :verification_via_multi_part) - post("/vyper-code", V2.VerificationController, :verification_via_vyper_code) - post("/vyper-multi-part", V2.VerificationController, :verification_via_vyper_multipart) - post("/vyper-standard-input", V2.VerificationController, :verification_via_vyper_standard_input) - end + post("/flattened-code", V2.VerificationController, :verification_via_flattened_code) + post("/standard-input", V2.VerificationController, :verification_via_standard_input) + post("/sourcify", V2.VerificationController, :verification_via_sourcify) + post("/multi-part", V2.VerificationController, :verification_via_multi_part) + post("/vyper-code", V2.VerificationController, :verification_via_vyper_code) + post("/vyper-multi-part", V2.VerificationController, :verification_via_vyper_multipart) + post("/vyper-standard-input", V2.VerificationController, :verification_via_vyper_standard_input) end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex new file mode 100644 index 0000000000..c9506cfc84 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex @@ -0,0 +1,71 @@ +# This file in ignore list of `sobelow`, be careful while adding new endpoints here +defmodule BlockScoutWeb.Routers.TokensApiV2Router do + @moduledoc """ + Router for /api/v2/tokens. This route has separate router in order to ignore sobelow's warning about missing CSRF protection + """ + use BlockScoutWeb, :router + alias BlockScoutWeb.API.V2 + alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + + @max_query_string_length 5_000 + + pipeline :api_v2 do + plug( + Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + query_string_length: @max_query_string_length, + pass: ["*/*"], + json_decoder: Poison + ) + + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) + plug(:accepts, ["json"]) + plug(CheckApiV2) + plug(:fetch_session) + plug(:protect_from_forgery) + plug(RateLimit) + end + + pipeline :api_v2_no_forgery_protect do + plug( + Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + length: 20_000_000, + query_string_length: 5_000, + pass: ["*/*"], + json_decoder: Poison + ) + + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) + plug(:accepts, ["json"]) + plug(CheckApiV2) + plug(RateLimit) + plug(:fetch_session) + end + + scope "/", as: :api_v2 do + pipe_through(:api_v2_no_forgery_protect) + + patch("/:address_hash_param/instances/:token_id/refetch-metadata", V2.TokenController, :refetch_metadata) + end + + scope "/", as: :api_v2 do + pipe_through(:api_v2) + + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + get("/bridged", V2.TokenController, :bridged_tokens_list) + end + + get("/", V2.TokenController, :tokens_list) + get("/:address_hash_param", V2.TokenController, :token) + get("/:address_hash_param/counters", V2.TokenController, :counters) + get("/:address_hash_param/transfers", V2.TokenController, :transfers) + get("/:address_hash_param/holders", V2.TokenController, :holders) + get("/:address_hash_param/holders/csv", V2.CSVExportController, :export_token_holders) + get("/:address_hash_param/instances", V2.TokenController, :instances) + get("/:address_hash_param/instances/:token_id", V2.TokenController, :instance) + get("/:address_hash_param/instances/:token_id/transfers", V2.TokenController, :transfers_by_instance) + get("/:address_hash_param/instances/:token_id/holders", V2.TokenController, :holders_by_instance) + get("/:address_hash_param/instances/:token_id/transfers-count", V2.TokenController, :transfers_count_by_instance) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex similarity index 94% rename from apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex rename to apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex index b251f928d7..2e9b8ef66f 100644 --- a/apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex @@ -1,5 +1,5 @@ # This file in ignore list of `sobelow`, be careful while adding new endpoints here -defmodule BlockScoutWeb.UtilsApiV2Router do +defmodule BlockScoutWeb.Routers.UtilsApiV2Router do @moduledoc """ Router for /api/v2/utils. This route has separate router in order to ignore sobelow's warning about missing CSRF protection """ diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/web_router.ex similarity index 99% rename from apps/block_scout_web/lib/block_scout_web/web_router.ex rename to apps/block_scout_web/lib/block_scout_web/routers/web_router.ex index cabf0ed4e3..2e8bce57fc 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/web_router.ex @@ -1,4 +1,4 @@ -defmodule BlockScoutWeb.WebRouter do +defmodule BlockScoutWeb.Routers.WebRouter do @moduledoc """ Router for web app """ diff --git a/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex index 77140e5843..af91a9122b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex @@ -17,7 +17,7 @@

-