feat: API endpoint to re-fetch token instance metadata (#10097)

* feat: Re-fetch token instance metadata

* Partially process review comments

* Process reviewer comments. Part 2

* Process reviewer comments. Part 3

* Process reviewer comments. Part 4

* Fix events

* Add test

* Remove :token preload

* fix formatting

* Fix tests

* Remove unused aliases

* Add reCAPTCHA for token instance re-fetch API endpoint

* Check event on websocket at /api/v2/tokens/{address_hash}/instances/{token_id}/refetch-metadata endpoint
pull/10240/head
Victor Baranov 5 months ago committed by GitHub
parent c31f937680
commit 4297704b8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      apps/block_scout_web/.sobelow-conf
  2. 4
      apps/block_scout_web/config/config.exs
  3. 6
      apps/block_scout_web/lib/block_scout_web.ex
  4. 26
      apps/block_scout_web/lib/block_scout_web/channels/token_instance_channel.ex
  5. 1
      apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex
  6. 1
      apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex
  7. 2
      apps/block_scout_web/lib/block_scout_web/controllers/admin/setup_controller.ex
  8. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex
  9. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex
  10. 41
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex
  11. 94
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  12. 8
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex
  13. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex
  14. 8
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex
  15. 11
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  16. 2
      apps/block_scout_web/lib/block_scout_web/plug/admin/check_owner_registered.ex
  17. 2
      apps/block_scout_web/lib/block_scout_web/plug/admin/require_admin_role.ex
  18. 1
      apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex
  19. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  20. 2
      apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex
  21. 2
      apps/block_scout_web/lib/block_scout_web/routers/api_key_v2_router.ex
  22. 25
      apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
  23. 46
      apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex
  24. 71
      apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex
  25. 2
      apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex
  26. 2
      apps/block_scout_web/lib/block_scout_web/routers/web_router.ex
  27. 2
      apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex
  28. 4
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  29. 4
      apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex
  30. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  31. 2
      apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
  32. 2
      apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex
  33. 2
      apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex
  34. 2
      apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex
  35. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs
  36. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs
  37. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
  38. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs
  39. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  40. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs
  41. 130
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
  42. 2
      apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs
  43. 2
      apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs
  44. 3
      apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
  45. 2
      apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
  46. 2
      apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs
  47. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs
  48. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs
  49. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs
  50. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
  51. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
  52. 2
      apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs
  53. 2
      apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs
  54. 2
      apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex
  55. 6
      apps/block_scout_web/test/support/conn_case.ex
  56. 1
      apps/block_scout_web/test/test_helper.exs
  57. 2
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
  58. 2
      apps/explorer/lib/explorer/account/notifier/email.ex
  59. 2
      apps/explorer/lib/explorer/chain/events/publisher.ex
  60. 2
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  61. 10
      apps/explorer/lib/explorer/chain/smart_contract.ex
  62. 21
      apps/explorer/lib/explorer/chain/token/instance.ex
  63. 4
      apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
  64. 15
      apps/explorer/lib/explorer/utility/address_contract_code_fetch_attempt.ex
  65. 87
      apps/explorer/lib/explorer/utility/token_instance_metadata_refetch_attempt.ex
  66. 41
      apps/explorer/lib/test_helper.ex
  67. 2
      apps/explorer/mix.exs
  68. 15
      apps/explorer/priv/repo/migrations/20240520075414_create_token_instance_metadata_refetch_attempts_table.exs
  69. 4
      apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs
  70. 2
      apps/indexer/lib/indexer/application.ex
  71. 2
      apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex
  72. 123
      apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex
  73. 32
      apps/indexer/lib/indexer/fetcher/token_instance/helper.ex
  74. 181
      apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs
  75. 3
      config/runtime.exs
  76. 1
      docker-compose/envs/common-blockscout.env

@ -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"
]
]

@ -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"),

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

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

@ -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)}

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

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

@ -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}

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

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

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

@ -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)

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

@ -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)

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

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

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

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

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

@ -1,4 +1,4 @@
defmodule BlockScoutWeb.AdminRouter do
defmodule BlockScoutWeb.Routers.AdminRouter do
@moduledoc """
Router for admin pages.
"""

@ -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
"""

@ -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)

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

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

@ -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
"""

@ -1,4 +1,4 @@
defmodule BlockScoutWeb.WebRouter do
defmodule BlockScoutWeb.Routers.WebRouter do
@moduledoc """
Router for web app
"""

@ -17,7 +17,7 @@
</p>
</div>
<button id="run-create-contract-methods" data-api_path="<%= BlockScoutWeb.AdminRouter.Helpers.create_contract_methods_path(@conn, :create_contract_methods) %>" class="btn btn-primary">
<button id="run-create-contract-methods" data-api_path="<%= BlockScoutWeb.Routers.AdminRouter.Helpers.create_contract_methods_path(@conn, :create_contract_methods) %>" class="btn btn-primary">
<%= gettext("Run") %>
<span data-loading-message class="loading-spinner-small mr-2 d-none">

@ -20,7 +20,7 @@
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav">
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.WebRouter)[:enabled] do %>
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.Routers.WebRouter)[:enabled] do %>
<li class="nav-item dropdown">
<a class="nav-link topnav-nav-link dropdown-toggle" href="#" id="navbarBlocksDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="nav-link-icon">
@ -97,7 +97,7 @@
</div>
</li>
<% end %>
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.ApiRouter)[:reading_enabled] || Application.get_env(:block_scout_web, :api_url) do %>
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.Routers.ApiRouter)[:reading_enabled] || Application.get_env(:block_scout_web, :api_url) do %>
<li class="nav-item dropdown">
<a href="#" role="button" id="navbarAPIsDropdown" class="nav-link topnav-nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="nav-link-icon">

@ -1,5 +1,5 @@
<% circles_addresses_list = CustomContractsHelper.get_custom_addresses_list(:circles_addresses) %>
<% address_hash_str = "0x" <> Base.encode16(@token.contract_address_hash.bytes, case: :lower) %>
<% address_hash_string = "0x" <> Base.encode16(@token.contract_address_hash.bytes, case: :lower) %>
<% {:ok, created_from_address} = if @token.contract_address_hash, do: Chain.hash_to_address(@token.contract_address_hash), else: {:ok, nil} %>
<% created_from_address_hash = if from_address_hash(created_from_address), do: "0x" <> Base.encode16(from_address_hash(created_from_address).bytes, case: :lower), else: nil %>
<section class="address-overview" data-page="token-details" data-page-address-hash="<%= @token.contract_address_hash %>">
@ -11,7 +11,7 @@
<h1 class="card-title d-flex" style="justify-content: space-between; margin-bottom: 0.75rem">
<div style="line-height: 30px;">
<%= cond do %>
<% Enum.member?(circles_addresses_list, address_hash_str) -> %>
<% Enum.member?(circles_addresses_list, address_hash_string) -> %>
<div class="custom-dapp-header-container">
<img class="custom-address-icon"/>
</div>

@ -23,7 +23,7 @@
<% decoded_input_data = decoded_input_data(@transaction) %>
<% status = transaction_status(@transaction) %>
<% circles_addresses_list = CustomContractsHelper.get_custom_addresses_list(:circles_addresses) %>
<% address_hash_str = if to_address_hash, do: "0x" <> Base.encode16(to_address_hash.bytes, case: :lower), else: nil %>
<% address_hash_string = if to_address_hash, do: "0x" <> Base.encode16(to_address_hash.bytes, case: :lower), else: nil %>
<% {:ok, created_from_address} = if to_address_hash, do: Chain.hash_to_address(to_address_hash), else: {:ok, nil} %>
<% created_from_address_hash_str = if from_address_hash(created_from_address), do: "0x" <> Base.encode16(from_address_hash(created_from_address).bytes, case: :lower), else: nil %>
<%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %>
@ -34,7 +34,7 @@
<div class="card js-ad-dependant-mb-3">
<div class="card-body">
<%= cond do %>
<% Enum.member?(circles_addresses_list, address_hash_str) -> %>
<% Enum.member?(circles_addresses_list, address_hash_string) -> %>
<div class="custom-dapp-header-container">
<img class="custom-address-icon"/>
</div>

@ -8,7 +8,7 @@ defmodule BlockScoutWeb.AccessHelper do
alias BlockScoutWeb.API.APILogger
alias BlockScoutWeb.API.RPC.RPCView
alias BlockScoutWeb.API.V2.ApiView
alias BlockScoutWeb.WebRouter.Helpers
alias BlockScoutWeb.Routers.WebRouter.Helpers
alias Explorer.AccessHelper
alias Explorer.Account.Api.Key, as: ApiKey
alias Plug.Conn

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.Admin.SessionView do
use BlockScoutWeb, :view
import BlockScoutWeb.AdminRouter.Helpers
import BlockScoutWeb.Routers.AdminRouter.Helpers
alias BlockScoutWeb.FormView
end

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.Admin.SetupView do
use BlockScoutWeb, :view
import BlockScoutWeb.AdminRouter.Helpers
import BlockScoutWeb.Routers.AdminRouter.Helpers
alias BlockScoutWeb.FormView
end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.VerifiedContractsView do
import BlockScoutWeb.AddressView, only: [balance: 1]
import BlockScoutWeb.Tokens.OverviewView, only: [total_supply_usd: 1]
alias BlockScoutWeb.WebRouter.Helpers
alias BlockScoutWeb.Routers.WebRouter.Helpers
def format_current_filter(filter) do
case filter do

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.AddressContractControllerTest do
use BlockScoutWeb.ConnCase, async: true
import BlockScoutWeb.WebRouter.Helpers, only: [address_contract_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_contract_path: 3]
alias Explorer.Chain.{Address, Hash}
alias Explorer.ExchangeRates.Token

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do
use BlockScoutWeb.ConnCase, async: true
import BlockScoutWeb.WebRouter.Helpers,
import BlockScoutWeb.Routers.WebRouter.Helpers,
only: [address_internal_transaction_path: 3, address_internal_transaction_path: 4]
alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction}

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
use BlockScoutWeb.ConnCase, async: true
use ExUnit.Case, async: false
import BlockScoutWeb.WebRouter.Helpers, only: [address_token_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_token_path: 3]
import Mox
alias Explorer.Chain.{Address, Token}

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.AddressTokenTransferControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers,
import BlockScoutWeb.Routers.WebRouter.Helpers,
only: [address_token_transfers_path: 4, address_token_transfers_path: 5]
alias Explorer.Chain.{Address, Token}

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
use BlockScoutWeb.ConnCase, async: true
use ExUnit.Case, async: false
import BlockScoutWeb.WebRouter.Helpers, only: [address_transaction_path: 3, address_transaction_path: 4]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_transaction_path: 3, address_transaction_path: 4]
import Mox
alias Explorer.Chain.{Address, Transaction}

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.AddressWithdrawalControllerTest do
use BlockScoutWeb.ConnCase, async: true
use ExUnit.Case, async: false
import BlockScoutWeb.WebRouter.Helpers, only: [address_withdrawal_path: 3, address_withdrawal_path: 4]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_withdrawal_path: 3, address_withdrawal_path: 4]
import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2]
import Mox

@ -1,11 +1,19 @@
defmodule BlockScoutWeb.API.V2.TokenControllerTest do
use EthereumJSONRPC.Case, async: false
use BlockScoutWeb.ConnCase
use BlockScoutWeb.ChannelCase, async: false
alias Explorer.Repo
import Mox
alias Explorer.Chain.{Address, Token, Token.Instance, TokenTransfer}
alias BlockScoutWeb.Notifier
alias Explorer.{Repo, TestHelper}
alias Explorer.Chain.{Address, Token, Token.Instance, TokenTransfer}
alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Events.Subscriber
alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand
describe "/tokens/{address_hash}" do
test "get 404 on non existing address", %{conn: conn} do
@ -1068,14 +1076,13 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
test "regression for #9906", %{conn: conn} do
token = insert(:token, type: "ERC-721")
instance =
insert(:token_instance,
token_id: 0,
token_contract_address_hash: token.contract_address_hash,
metadata: %{
"image_url" => "ipfs://QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/gm%20gm%20feelin%20blue%204k.png"
}
)
insert(:token_instance,
token_id: 0,
token_contract_address_hash: token.contract_address_hash,
metadata: %{
"image_url" => "ipfs://QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/gm%20gm%20feelin%20blue%204k.png"
}
)
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0")
@ -1444,6 +1451,109 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
end
end
describe "/tokens/{address_hash}/instances/{token_id}/refetch-metadata" do
setup :set_mox_from_context
setup :verify_on_exit!
setup %{json_rpc_named_arguments: json_rpc_named_arguments} do
mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox)
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
start_supervised!(
{TokenInstanceMetadataRefetchOnDemand,
[mocked_json_rpc_named_arguments, [name: TokenInstanceMetadataRefetchOnDemand]]}
)
%{json_rpc_named_arguments: mocked_json_rpc_named_arguments}
Subscriber.to(:fetched_token_instance_metadata, :on_demand)
:ok
end
test "token instance metadata on-demand re-fetcher is called", %{conn: conn} do
BlockScoutWeb.TestCaptchaHelper
|> expect(:recaptcha_passed?, fn _captcha_response -> true end)
token = insert(:token, type: "ERC-721")
token_id = 1
insert(:token_instance,
token_id: token_id,
token_contract_address_hash: token.contract_address_hash,
metadata: %{}
)
metadata = %{"name" => "Super Token"}
url = "http://metadata.endpoint.com"
token_contract_address_hash_string = to_string(token.contract_address_hash)
TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string)
Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison)
Explorer.Mox.HTTPoison
|> expect(:get, fn ^url, _headers, _options ->
{:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}}
end)
topic = "token_instances:#{token_contract_address_hash_string}"
{:ok, _reply, _socket} =
BlockScoutWeb.UserSocketV2
|> socket("no_id", %{})
|> subscribe_and_join(topic)
request =
patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{
"recaptcha_response" => "123"
})
assert %{"message" => "OK"} = json_response(request, 200)
:timer.sleep(100)
assert_receive(
{:chain_event, :fetched_token_instance_metadata, :on_demand,
[^token_contract_address_hash_string, ^token_id, ^metadata]}
)
assert_receive %Phoenix.Socket.Message{
payload: %{token_id: ^token_id, fetched_metadata: ^metadata},
event: "fetched_token_instance_metadata",
topic: ^topic
},
:timer.seconds(1)
token_instance_from_db =
Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash)
assert(token_instance_from_db)
assert token_instance_from_db.metadata == metadata
Application.put_env(:explorer, :http_adapter, HTTPoison)
end
test "don't fetch token instance metadata for non-existent token instance", %{conn: conn} do
BlockScoutWeb.TestCaptchaHelper
|> expect(:recaptcha_passed?, fn _captcha_response -> true end)
token = insert(:token, type: "ERC-721")
token_id = 0
insert(:token_instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash)
request =
patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/1/refetch-metadata", %{
"recaptcha_response" => "123"
})
assert %{"message" => "Not found"} = json_response(request, 404)
end
end
def compare_item(%Address{} = address, json) do
assert Address.checksum(address.hash) == json["hash"]
end

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.BlockTransactionControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [block_transaction_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [block_transaction_path: 3]
describe "GET index/2" do
test "with invalid block number", %{conn: conn} do

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.BlockWithdrawalControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [block_withdrawal_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [block_withdrawal_path: 3]
describe "GET index/2" do
test "with invalid block number", %{conn: conn} do

@ -3,7 +3,8 @@ defmodule BlockScoutWeb.ChainControllerTest do
# ETS table is shared in `Explorer.Counters.AddressesCounter`
async: false
import BlockScoutWeb.WebRouter.Helpers, only: [chain_path: 2, block_path: 3, transaction_path: 3, address_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers,
only: [chain_path: 2, block_path: 3, transaction_path: 3, address_path: 3]
alias Explorer.Chain.Block
alias Explorer.Counters.AddressesCounter

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.PendingTransactionControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Hash, Transaction}
import BlockScoutWeb.WebRouter.Helpers, only: [pending_transaction_path: 2, pending_transaction_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [pending_transaction_path: 2, pending_transaction_path: 3]
describe "GET index/2" do
test "returns no transactions that are in a block", %{conn: conn} do

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.RecentTransactionsControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [recent_transactions_path: 2]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [recent_transactions_path: 2]
alias Explorer.Chain.Hash

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.TransactionControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers,
import BlockScoutWeb.Routers.WebRouter.Helpers,
only: [transaction_path: 3]
alias Explorer.Chain.Transaction

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [transaction_internal_transaction_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_internal_transaction_path: 3]
alias Explorer.Chain.InternalTransaction
alias Explorer.ExchangeRates.Token

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.TransactionLogControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [transaction_log_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_log_path: 3]
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do
import Mox
import BlockScoutWeb.WebRouter.Helpers, only: [transaction_state_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_state_path: 3]
import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2]
import EthereumJSONRPC, only: [integer_to_quantity: 1]
alias Explorer.Chain.Wei

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do
import Mox
import BlockScoutWeb.WebRouter.Helpers, only: [transaction_token_transfer_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_token_transfer_path: 3]
alias Explorer.ExchangeRates.Token
alias Explorer.TestHelper

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.VerifiedContractsControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [verified_contracts_path: 2, verified_contracts_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [verified_contracts_path: 2, verified_contracts_path: 3]
alias Explorer.Chain.SmartContract

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.WithdrawalControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.WebRouter.Helpers, only: [withdrawal_path: 2, withdrawal_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [withdrawal_path: 2, withdrawal_path: 3]
alias Explorer.Chain.Withdrawal

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.TransactionLogsPage do
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
import BlockScoutWeb.WebRouter.Helpers, only: [transaction_log_path: 3]
import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_log_path: 3]
alias Explorer.Chain.Transaction
alias BlockScoutWeb.Endpoint

@ -21,7 +21,7 @@ defmodule BlockScoutWeb.ConnCase do
import Plug.Conn
import Phoenix.ConnTest
import BlockScoutWeb.Router.Helpers
import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2]
import BlockScoutWeb.Routers.WebRouter.Helpers, except: [static_path: 2]
import BlockScoutWeb.Routers.AccountRouter.Helpers, except: [static_path: 2]
import Bureaucrat.Helpers
@ -30,8 +30,8 @@ defmodule BlockScoutWeb.ConnCase do
import Explorer.Factory
alias BlockScoutWeb.AdminRouter.Helpers, as: AdminRoutes
alias BlockScoutWeb.ApiRouter.Helpers, as: ApiRoutes
alias BlockScoutWeb.Routers.AdminRouter.Helpers, as: AdminRoutes
alias BlockScoutWeb.Routers.ApiRouter.Helpers, as: ApiRoutes
end
end

@ -42,3 +42,4 @@ Absinthe.Test.prime(BlockScoutWeb.GraphQL.Schema)
Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport)
Mox.defmock(BlockScoutWeb.TestCaptchaHelper, for: BlockScoutWeb.CaptchaHelper)
Mox.defmock(Explorer.Mox.HTTPoison, for: HTTPoison.Base)

@ -18,7 +18,7 @@ defmodule EthereumJSONRPC.Contract do
required(:contract_address) => String.t(),
required(:method_id) => String.t(),
required(:args) => [term()],
optional(:block_number) => EthereumJSONRPC.block_number()
optional(:block_number) => EthereumJSONRPC.block_number() | nil
}
@typedoc """

@ -5,7 +5,7 @@ defmodule Explorer.Account.Notifier.Email do
require Logger
alias BlockScoutWeb.WebRouter.Helpers
alias BlockScoutWeb.Routers.WebRouter.Helpers
alias Explorer.Account.{Identity, Watchlist, WatchlistAddress, WatchlistNotification}
alias Explorer.Repo

@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Publisher do
Publishes events related to the Chain context.
"""
@allowed_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number optimism_deposits token_transfers transactions contract_verification_result token_total_supply changed_bytecode fetched_bytecode smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a
@allowed_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number optimism_deposits token_transfers transactions contract_verification_result token_total_supply changed_bytecode fetched_bytecode fetched_token_instance_metadata smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a
def broadcast(_data, false), do: :ok

@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Subscriber do
Subscribes to events related to the Chain context.
"""
@allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number optimism_deposits token_transfers transactions contract_verification_result token_total_supply changed_bytecode fetched_bytecode smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a
@allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number optimism_deposits token_transfers transactions contract_verification_result token_total_supply changed_bytecode fetched_bytecode fetched_token_instance_metadata smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a
@allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a

@ -1004,8 +1004,8 @@ defmodule Explorer.Chain.SmartContract do
@spec verified_with_full_match?(Hash.Address.t() | String.t()) :: boolean()
def verified_with_full_match?(address_hash, options \\ [])
def verified_with_full_match?(address_hash_str, options) when is_binary(address_hash_str) do
case Chain.string_to_address_hash(address_hash_str) do
def verified_with_full_match?(address_hash_string, options) when is_binary(address_hash_string) do
case Chain.string_to_address_hash(address_hash_string) do
{:ok, address_hash} ->
check_verified_with_full_match(address_hash, options)
@ -1022,15 +1022,15 @@ defmodule Explorer.Chain.SmartContract do
Checks if a `Explorer.Chain.SmartContract` exists for the provided address hash.
## Parameters
- `address_hash_str` or `address_hash`: The hash of the address in binary string
- `address_hash_string` or `address_hash`: The hash of the address in binary string
form or directly as an address hash.
## Returns
- `boolean()`: `true` if a smart contract exists, `false` otherwise.
"""
@spec verified?(Hash.Address.t() | String.t()) :: boolean()
def verified?(address_hash_str) when is_binary(address_hash_str) do
case Chain.string_to_address_hash(address_hash_str) do
def verified?(address_hash_string) when is_binary(address_hash_string) do
case Chain.string_to_address_hash(address_hash_string) do
{:ok, address_hash} ->
verified_smart_contract_exists?(address_hash)

@ -5,12 +5,14 @@ defmodule Explorer.Chain.Token.Instance do
use Explorer.Schema
alias Explorer.{Chain, Helper}
alias Explorer.{Chain, Helper, Repo}
alias Explorer.Chain.{Address, Hash, Token, TokenTransfer}
alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Token.Instance
alias Explorer.PagingOptions
@timeout 60_000
@typedoc """
* `token_id` - ID of the token
* `token_contract_address_hash` - Address hash foreign key
@ -617,4 +619,21 @@ defmodule Explorer.Chain.Token.Instance do
do:
not (token.type == "ERC-1155") or
Chain.token_id_1155_is_unique?(token.contract_address_hash, instance.token_id, options)
@doc """
Sets set_metadata for the given Explorer.Chain.Token.Instance
"""
@spec set_metadata(__MODULE__, map()) :: {non_neg_integer(), nil}
def set_metadata(token_instance, metadata) when is_map(metadata) do
now = DateTime.utc_now()
Repo.update_all(
from(instance in __MODULE__,
where: instance.token_contract_address_hash == ^token_instance.token_contract_address_hash,
where: instance.token_id == ^token_instance.token_id
),
[set: [metadata: metadata, error: nil, updated_at: now]],
timeout: @timeout
)
end
end

@ -148,8 +148,8 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
def source_url(input) do
case Chain.Hash.Address.cast(input) do
{:ok, _} ->
address_hash_str = input
"#{base_url()}/coins/#{platform()}/contract/#{address_hash_str}"
address_hash_string = input
"#{base_url()}/coins/#{platform()}/contract/#{address_hash_string}"
_ ->
symbol = input

@ -39,13 +39,24 @@ defmodule Explorer.Utility.AddressContractCodeFetchAttempt do
@doc """
Deletes row from address_contract_code_fetch_attempts table for the given address
"""
@spec delete_address_contract_code_fetch_attempt(Hash.Address.t()) :: {non_neg_integer(), nil}
def delete_address_contract_code_fetch_attempt(address_hash) do
@spec delete(Hash.Address.t()) :: {non_neg_integer(), nil}
def delete(address_hash) do
__MODULE__
|> where([address_contract_code_fetch_attempt], address_contract_code_fetch_attempt.address_hash == ^address_hash)
|> Repo.delete_all()
end
@doc """
Inserts the number of retries for fetching contract code for a given address.
## Parameters
- `address_hash` - The hash of the address for which the retries number is to be inserted.
## Returns
The result of the insertion operation.
"""
@spec insert_retries_number(Hash.Address.t()) :: {non_neg_integer(), nil | [term()]}
def insert_retries_number(address_hash) do
now = DateTime.utc_now()
params = [%{address_hash: address_hash, inserted_at: now, updated_at: now, retries_number: 1}]

@ -0,0 +1,87 @@
defmodule Explorer.Utility.TokenInstanceMetadataRefetchAttempt do
@moduledoc """
Module is responsible for keeping the number of retries for
Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch.
"""
use Explorer.Schema
alias Explorer.Chain.Hash
alias Explorer.Repo
@primary_key false
typed_schema "token_instance_metadata_refetch_attempts" do
field(:token_contract_address_hash, Hash.Address, primary_key: true)
field(:token_id, :decimal, primary_key: true)
field(:retries_number, :integer, primary_key: false)
timestamps()
end
@doc false
def changeset(token_instance_metadata_refetch_attempt \\ %__MODULE__{}, params) do
cast(token_instance_metadata_refetch_attempt, params, [:hash, :retries_number])
end
@doc """
Gets retries number and updated_at for given token contract Explorer.Chain.Address and token_id
"""
@spec get_retries_number(Hash.Address.t(), non_neg_integer()) :: {non_neg_integer(), DateTime.t()} | nil
def get_retries_number(token_contract_address_hash, token_id) do
__MODULE__
|> where(
[token_instance_metadata_refetch_attempt],
token_instance_metadata_refetch_attempt.token_contract_address_hash == ^token_contract_address_hash
)
|> where([token_instance_metadata_refetch_attempt], token_instance_metadata_refetch_attempt.token_id == ^token_id)
|> select(
[token_instance_metadata_refetch_attempt],
{token_instance_metadata_refetch_attempt.retries_number, token_instance_metadata_refetch_attempt.updated_at}
)
|> Repo.one()
end
@doc """
Inserts the number of retries for fetching token instance metadata into the database.
## Parameters
- `token_contract_address_hash` - The hash of the token contract address.
- `token_id` - The ID of the token instance.
## Returns
The result of the insertion operation.
"""
@spec insert_retries_number(Hash.Address.t(), non_neg_integer()) :: {non_neg_integer(), nil | [term()]}
def insert_retries_number(token_contract_address_hash, token_id) do
now = DateTime.utc_now()
params = [
%{
token_contract_address_hash: token_contract_address_hash,
token_id: token_id,
inserted_at: now,
updated_at: now,
retries_number: 1
}
]
Repo.insert_all(__MODULE__, params,
on_conflict: default_on_conflict(),
conflict_target: [:token_contract_address_hash, :token_id]
)
end
defp default_on_conflict do
from(
token_instance_metadata_refetch_attempt in __MODULE__,
update: [
set: [
retries_number: fragment("? + 1", token_instance_metadata_refetch_attempt.retries_number),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token_instance_metadata_refetch_attempt.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token_instance_metadata_refetch_attempt.updated_at)
]
]
)
end
end

@ -3,6 +3,8 @@ defmodule Explorer.TestHelper do
import Mox
alias ABI.TypeEncoder
def mock_logic_storage_pointer_request(
mox,
error?,
@ -109,4 +111,43 @@ defmodule Explorer.TestHelper do
|> mock_oz_storage_pointer_request(true)
|> mock_eip_1822_storage_pointer_request(true)
end
def fetch_token_uri_mock(url, token_contract_address_hash_string) do
encoded_url =
"0x" <>
([url]
|> TypeEncoder.encode(%ABI.FunctionSelector{
function: nil,
types: [
:string
]
})
|> Base.encode16(case: :lower))
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data: "0xc87b56dd0000000000000000000000000000000000000000000000000000000000000001",
to: ^token_contract_address_hash_string
},
"latest"
]
}
],
_options ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result: encoded_url
}
]}
end)
end
end

@ -25,7 +25,7 @@ defmodule Explorer.Mixfile do
],
start_permanent: Mix.env() == :prod,
version: "6.6.0",
xref: [exclude: [BlockScoutWeb.WebRouter.Helpers, Indexer.Helper]]
xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper]]
]
end

@ -0,0 +1,15 @@
defmodule Explorer.Repo.Migrations.CreateTokenInstanceMetadataRefetchAttemptsTable do
use Ecto.Migration
def change do
create table(:token_instance_metadata_refetch_attempts, primary_key: false) do
add(:token_contract_address_hash, :bytea, null: false, primary_key: true)
add(:token_id, :numeric, precision: 78, scale: 0, null: false, primary_key: true)
add(:retries_number, :smallint)
timestamps()
end
create(index(:token_instance_metadata_refetch_attempts, [:token_contract_address_hash, :token_id]))
end
end

@ -7,8 +7,8 @@ defmodule Explorer.SmartContract.Vyper.PublisherTest do
@moduletag timeout: :infinity
alias Explorer.Chain.{ContractMethod, SmartContract}
alias Explorer.{Factory, Repo}
alias Explorer.Chain.{SmartContract}
alias Explorer.Factory
alias Explorer.SmartContract.Vyper.Publisher
setup do

@ -8,6 +8,7 @@ defmodule Indexer.Application do
alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand
alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand
alias Indexer.Fetcher.OnDemand.FirstTrace, as: FirstTraceOnDemand
alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand
alias Indexer.Fetcher.OnDemand.TokenTotalSupply, as: TokenTotalSupplyOnDemand
alias Indexer.Memory
@ -50,6 +51,7 @@ defmodule Indexer.Application do
{Memory.Monitor, [memory_monitor_options, [name: memory_monitor_name]]},
{CoinBalanceOnDemand.Supervisor, [json_rpc_named_arguments]},
{ContractCodeOnDemand.Supervisor, [json_rpc_named_arguments]},
{TokenInstanceMetadataRefetchOnDemand.Supervisor, [json_rpc_named_arguments]},
{TokenTotalSupplyOnDemand.Supervisor, []},
{FirstTraceOnDemand.Supervisor, [json_rpc_named_arguments]}
]

@ -59,7 +59,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do
true <- contract_code_object.code !== "0x" do
case Address.set_contract_code(address_hash, contract_code_object.code) do
{1, _} ->
AddressContractCodeFetchAttempt.delete_address_contract_code_fetch_attempt(address_hash)
AddressContractCodeFetchAttempt.delete(address_hash)
Publisher.broadcast(%{fetched_bytecode: [address_hash, contract_code_object.code]}, :on_demand)
_ ->

@ -0,0 +1,123 @@
defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch do
@moduledoc """
Re-fetches token instance metadata.
"""
require Logger
use GenServer
use Indexer.Fetcher, restart: :permanent
alias Explorer.Chain.Events.Publisher
alias Explorer.Chain.Token.Instance, as: TokenInstance
alias Explorer.Counters.Helper, as: CountersHelper
alias Explorer.SmartContract.Reader
alias Explorer.Token.MetadataRetriever
alias Explorer.Utility.TokenInstanceMetadataRefetchAttempt
alias Indexer.Fetcher.TokenInstance.Helper, as: TokenInstanceHelper
@max_delay :timer.hours(168)
@spec trigger_refetch(TokenInstance.t()) :: :ok
def trigger_refetch(token_instance) do
unless is_nil(token_instance.metadata) do
GenServer.cast(__MODULE__, {:refetch, token_instance})
end
end
defp fetch_metadata(token_instance, state) do
with {:retries_number, {retries_number, updated_at}} <-
{:retries_number,
TokenInstanceMetadataRefetchAttempt.get_retries_number(
token_instance.token_contract_address_hash,
token_instance.token_id
)},
updated_at_ms = DateTime.to_unix(updated_at, :millisecond),
{:retry, true} <-
{:retry,
CountersHelper.current_time() - updated_at_ms >
threshold(retries_number)} do
fetch_and_broadcast_metadata(token_instance, state)
else
{:retries_number, nil} ->
fetch_and_broadcast_metadata(token_instance, state)
{:retry, false} ->
:ok
end
end
defp fetch_and_broadcast_metadata(token_instance, _state) do
from_base_uri? = Application.get_env(:indexer, __MODULE__)[:base_uri_retry?]
token_id = TokenInstanceHelper.prepare_token_id(token_instance.token_id)
contract_address_hash_string = to_string(token_instance.token_contract_address_hash)
request =
TokenInstanceHelper.prepare_request(
token_instance.token.type,
contract_address_hash_string,
token_id,
false
)
result =
case Reader.query_contracts([request], TokenInstanceHelper.erc_721_1155_abi(), [], false) do
[ok: [uri]] ->
{:ok, [uri]}
_ ->
nil
end
with {:empty_result, false} <- {:empty_result, is_nil(result)},
{:fetched_metadata, {:ok, %{metadata: metadata}}} <-
{:fetched_metadata, MetadataRetriever.fetch_json(result, token_id, nil, from_base_uri?)} do
TokenInstance.set_metadata(token_instance, metadata)
Publisher.broadcast(
%{fetched_token_instance_metadata: [to_string(token_instance.token_contract_address_hash), token_id, metadata]},
:on_demand
)
else
{:empty_result, true} ->
:ok
{:fetched_metadata, _error} ->
Logger.error(fn ->
"Error while setting address #{inspect(to_string(token_instance.token_contract_address_hash))} metadata"
end)
TokenInstanceMetadataRefetchAttempt.insert_retries_number(
token_instance.token_contract_address_hash,
token_instance.token_id
)
end
end
def start_link([init_opts, server_opts]) do
GenServer.start_link(__MODULE__, init_opts, server_opts)
end
@impl true
def init(json_rpc_named_arguments) do
{:ok, %{json_rpc_named_arguments: json_rpc_named_arguments}}
end
@impl true
def handle_cast({:refetch, token_instance}, state) do
fetch_metadata(token_instance, state)
{:noreply, state}
end
defp update_threshold_ms do
Application.get_env(:indexer, __MODULE__)[:threshold]
end
defp threshold(retries_number) do
delay_in_ms = trunc(update_threshold_ms() * :math.pow(2, retries_number))
min(delay_in_ms, @max_delay)
end
end

@ -218,11 +218,15 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do
|> Enum.zip(contract_results)
end
defp prepare_token_id(%Decimal{} = token_id), do: Decimal.to_integer(token_id)
defp prepare_token_id(token_id), do: token_id
@doc """
Prepares token id for request.
"""
@spec prepare_token_id(any) :: any
def prepare_token_id(%Decimal{} = token_id), do: Decimal.to_integer(token_id)
def prepare_token_id(token_id), do: token_id
defp prepare_request(erc_721_404, contract_address_hash_string, token_id, from_base_uri?)
when erc_721_404 in ["ERC-404", "ERC-721"] do
def prepare_request(erc_721_404, contract_address_hash_string, token_id, from_base_uri?)
when erc_721_404 in ["ERC-404", "ERC-721"] do
request = %{
contract_address: contract_address_hash_string,
block_number: nil
@ -235,7 +239,7 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do
end
end
defp prepare_request(_token_type, contract_address_hash_string, token_id, _retry) do
def prepare_request(_token_type, contract_address_hash_string, token_id, _retry) do
%{
contract_address: contract_address_hash_string,
method_id: @uri,
@ -244,9 +248,9 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do
}
end
defp normalize_token_id("ERC-721", _token_id), do: nil
def normalize_token_id("ERC-721", _token_id), do: nil
defp normalize_token_id(_token_type, token_id),
def normalize_token_id(_token_type, token_id),
do: token_id |> Integer.to_string(16) |> String.downcase() |> String.pad_leading(64, "0")
defp result_to_insert_params({:ok, %{metadata: metadata}}, token_contract_address_hash, token_id) do
@ -303,4 +307,18 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do
|> upsert_with_rescue(token_id, token_contract_address_hash, true)
end
end
@doc """
Returns the ABI of uri, tokenURI, baseURI getters for ERC721 and ERC1155 tokens.
"""
def erc_721_1155_abi do
@erc_721_1155_abi
end
@doc """
Returns tokenURI method signature.
"""
def token_uri do
@token_uri
end
end

@ -0,0 +1,181 @@
defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do
use EthereumJSONRPC.Case, async: false
use Explorer.DataCase
import Mox
alias Explorer.Chain.Token.Instance, as: TokenInstance
alias Explorer.Chain.Events.Subscriber
alias Explorer.TestHelper
alias Explorer.Utility.TokenInstanceMetadataRefetchAttempt
alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand
@moduletag :capture_log
setup :set_mox_global
setup :verify_on_exit!
setup %{json_rpc_named_arguments: json_rpc_named_arguments} do
mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox)
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
start_supervised!(
{TokenInstanceMetadataRefetchOnDemand,
[mocked_json_rpc_named_arguments, [name: TokenInstanceMetadataRefetchOnDemand]]}
)
%{json_rpc_named_arguments: mocked_json_rpc_named_arguments}
end
describe "refetch token instance metadata behaviour" do
setup do
Subscriber.to(:fetched_token_instance_metadata, :on_demand)
:ok
end
test "token instance broadcasts fetched token instance metadata" do
token = insert(:token, name: "Super Token", type: "ERC-721")
token_id = 1
token_instance =
insert(:token_instance,
token_id: token_id,
token_contract_address_hash: token.contract_address_hash,
metadata: %{}
)
|> Repo.preload(:token)
metadata = %{"name" => "Super Token"}
url = "http://metadata.endpoint.com"
token_contract_address_hash_string = to_string(token.contract_address_hash)
TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string)
Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison)
Explorer.Mox.HTTPoison
|> expect(:get, fn ^url, _headers, _options ->
{:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}}
end)
assert TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance) == :ok
:timer.sleep(100)
token_instance_from_db =
Repo.get_by(TokenInstance, token_id: token_id, token_contract_address_hash: token.contract_address_hash)
assert(token_instance_from_db)
refute is_nil(token_instance_from_db.metadata)
assert token_instance_from_db.metadata == metadata
assert is_nil(
Repo.get_by(TokenInstanceMetadataRefetchAttempt,
token_contract_address_hash: token.contract_address_hash,
token_id: token_id
)
)
assert_receive(
{:chain_event, :fetched_token_instance_metadata, :on_demand,
[^token_contract_address_hash_string, ^token_id, ^metadata]}
)
Application.put_env(:explorer, :http_adapter, HTTPoison)
end
test "don't run the update on the token instance with no metadata fetched initially" do
token = insert(:token, name: "Super Token", type: "ERC-721")
token_id = 1
token_instance =
insert(:token_instance,
token_id: token_id,
token_contract_address_hash: token.contract_address_hash,
metadata: nil
)
|> Repo.preload(:token)
metadata = %{"name" => "Super Token"}
token_contract_address_hash_string = to_string(token.contract_address_hash)
assert TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance) == nil
:timer.sleep(100)
token_instance_from_db =
Repo.get_by(TokenInstance, token_id: token_id, token_contract_address_hash: token.contract_address_hash)
assert(token_instance_from_db)
assert is_nil(token_instance_from_db.metadata)
assert is_nil(
Repo.get_by(TokenInstanceMetadataRefetchAttempt,
token_contract_address_hash: token.contract_address_hash,
token_id: token_id
)
)
refute_receive(
{:chain_event, :fetched_token_instance_metadata, :on_demand,
[^token_contract_address_hash_string, ^token_id, %{metadata: ^metadata}]}
)
end
test "updates token_instance_metadata_refetch_attempts table" do
token = insert(:token, name: "Super Token", type: "ERC-721")
token_id = 1
token_instance =
insert(:token_instance,
token_id: token_id,
token_contract_address_hash: token.contract_address_hash,
metadata: %{}
)
|> Repo.preload(:token)
metadata = %{"name" => "Super Token"}
url = "http://metadata.endpoint.com"
token_contract_address_hash_string = to_string(token.contract_address_hash)
TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string)
Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison)
Explorer.Mox.HTTPoison
|> expect(:get, fn ^url, _headers, _options ->
{:ok, %HTTPoison.Response{status_code: 200, body: nil}}
end)
assert TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance) == :ok
:timer.sleep(100)
token_instance_from_db =
Repo.get_by(TokenInstance, token_id: token_id, token_contract_address_hash: token.contract_address_hash)
assert(token_instance_from_db)
refute is_nil(token_instance_from_db.metadata)
attempts =
Repo.get_by(TokenInstanceMetadataRefetchAttempt,
token_contract_address_hash: token.contract_address_hash,
token_id: token_id
)
refute is_nil(attempts)
assert attempts.retries_number == 1
refute_receive(
{:chain_event, :fetched_token_instance_metadata, :on_demand,
[^token_contract_address_hash_string, ^token_id, %{metadata: ^metadata}]}
)
Application.put_env(:explorer, :http_adapter, HTTPoison)
end
end
end

@ -676,6 +676,9 @@ config :indexer, Indexer.Fetcher.OnDemand.CoinBalance,
config :indexer, Indexer.Fetcher.OnDemand.ContractCode,
threshold: ConfigHelper.parse_time_env_var("CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD", "5s")
config :indexer, Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch,
threshold: ConfigHelper.parse_time_env_var("TOKEN_INSTANCE_METADATA_REFETCH_ON_DEMAND_FETCHER_THRESHOLD", "5s")
config :indexer, Indexer.Fetcher.BlockReward.Supervisor,
disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_BLOCK_REWARD_FETCHER")

@ -105,6 +105,7 @@ CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD=1800
# TOKEN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD=
# COIN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD=
# CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD=
# TOKEN_INSTANCE_METADATA_REFETCH_ON_DEMAND_FETCHER_THRESHOLD=
TOKEN_METADATA_UPDATE_INTERVAL=172800
CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,default
CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default

Loading…
Cancel
Save