<%= cond do %>
- <% Enum.member?(circles_addresses_list, address_hash_str) -> %>
+ <% Enum.member?(circles_addresses_list, address_hash_string) -> %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
index a73d470120..3ba1c80494 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
@@ -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 @@
<%= cond do %>
- <% Enum.member?(circles_addresses_list, address_hash_str) -> %>
+ <% Enum.member?(circles_addresses_list, address_hash_string) -> %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
index 8d200b36de..d56b2dd8c8 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex b/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex
index 6e99ff6711..0c0080118f 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex b/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex
index 75f8a36f04..ab2df94f5f 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex
@@ -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
diff --git a/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex b/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex
index 8a6e837d0a..50934a2afe 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs
index 9e3be100ae..25579b886d 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs
index 1750f9bab1..d28b77fa9e 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs
@@ -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}
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
index 475987d235..41f666bf15 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
@@ -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}
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs
index f8a6602b8f..7d6bac1bf8 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs
@@ -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}
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
index 4c0c8ea80e..04dd677be9 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
@@ -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}
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs
index 557c906ac1..9c6163e2a5 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
index dadae9f101..814c55959f 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs
index d76a2fcd1c..494dc0d909 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs
index 61d3d76c46..831d088286 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
index 246ab7f7b2..a6247ba2f0 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
index b19bec2520..c8e57ddb64 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs
index 0a2267f18c..e0d2e56f5b 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs
index c94d095084..65d8f89469 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs
index 71deea5dbf..7b5078889c 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs
index 57f8933c24..27db716f03 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
index a652d15a75..0c5417ffb5 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
index a50bd9fb63..5d7fa34b80 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs
index 55936934dc..b7ac5eb7e0 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs
index d62fb54191..2ce5bea711 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs
@@ -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
diff --git a/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex b/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex
index 3969cd559c..cd0dc88da8 100644
--- a/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex
+++ b/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex
@@ -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
diff --git a/apps/block_scout_web/test/support/conn_case.ex b/apps/block_scout_web/test/support/conn_case.ex
index cd03ad31e1..d122090b4a 100644
--- a/apps/block_scout_web/test/support/conn_case.ex
+++ b/apps/block_scout_web/test/support/conn_case.ex
@@ -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
diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs
index ee03ef1827..d03847979f 100644
--- a/apps/block_scout_web/test/test_helper.exs
+++ b/apps/block_scout_web/test/test_helper.exs
@@ -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)
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
index 64ed49911d..5bef1c21d6 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
@@ -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 """
diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex
index 6e8639e058..a47a0155ae 100644
--- a/apps/explorer/lib/explorer/account/notifier/email.ex
+++ b/apps/explorer/lib/explorer/account/notifier/email.ex
@@ -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
diff --git a/apps/explorer/lib/explorer/chain/events/publisher.ex b/apps/explorer/lib/explorer/chain/events/publisher.ex
index 52791bbd3c..55bdd4a217 100644
--- a/apps/explorer/lib/explorer/chain/events/publisher.ex
+++ b/apps/explorer/lib/explorer/chain/events/publisher.ex
@@ -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
diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex
index dbe7f370b3..f599822341 100644
--- a/apps/explorer/lib/explorer/chain/events/subscriber.ex
+++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex
@@ -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
diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex
index d295288b60..cfac2ed864 100644
--- a/apps/explorer/lib/explorer/chain/smart_contract.ex
+++ b/apps/explorer/lib/explorer/chain/smart_contract.ex
@@ -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)
diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex
index 52f26d3c2c..ee3422dfff 100644
--- a/apps/explorer/lib/explorer/chain/token/instance.ex
+++ b/apps/explorer/lib/explorer/chain/token/instance.ex
@@ -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
diff --git a/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex b/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
index 80ba6bcd9f..4dd140d5ac 100644
--- a/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
+++ b/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
@@ -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
diff --git a/apps/explorer/lib/explorer/utility/address_contract_code_fetch_attempt.ex b/apps/explorer/lib/explorer/utility/address_contract_code_fetch_attempt.ex
index 03f50fbe2e..89f5672dac 100644
--- a/apps/explorer/lib/explorer/utility/address_contract_code_fetch_attempt.ex
+++ b/apps/explorer/lib/explorer/utility/address_contract_code_fetch_attempt.ex
@@ -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}]
diff --git a/apps/explorer/lib/explorer/utility/token_instance_metadata_refetch_attempt.ex b/apps/explorer/lib/explorer/utility/token_instance_metadata_refetch_attempt.ex
new file mode 100644
index 0000000000..a40e754d36
--- /dev/null
+++ b/apps/explorer/lib/explorer/utility/token_instance_metadata_refetch_attempt.ex
@@ -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
diff --git a/apps/explorer/lib/test_helper.ex b/apps/explorer/lib/test_helper.ex
index f427b00e30..e82a742b83 100644
--- a/apps/explorer/lib/test_helper.ex
+++ b/apps/explorer/lib/test_helper.ex
@@ -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
diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs
index d51224213f..d4d8d5a9c9 100644
--- a/apps/explorer/mix.exs
+++ b/apps/explorer/mix.exs
@@ -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
diff --git a/apps/explorer/priv/repo/migrations/20240520075414_create_token_instance_metadata_refetch_attempts_table.exs b/apps/explorer/priv/repo/migrations/20240520075414_create_token_instance_metadata_refetch_attempts_table.exs
new file mode 100644
index 0000000000..5d15ef1db2
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20240520075414_create_token_instance_metadata_refetch_attempts_table.exs
@@ -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
diff --git a/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs b/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs
index 5eeee2e4a8..be977cbd48 100644
--- a/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs
+++ b/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs
@@ -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
diff --git a/apps/indexer/lib/indexer/application.ex b/apps/indexer/lib/indexer/application.ex
index 21a5c1b01b..4003de22f9 100644
--- a/apps/indexer/lib/indexer/application.ex
+++ b/apps/indexer/lib/indexer/application.ex
@@ -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]}
]
diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex
index 5631e93c78..3b4823005a 100644
--- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex
+++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex
@@ -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)
_ ->
diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex
new file mode 100644
index 0000000000..652b4b3a74
--- /dev/null
+++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex
@@ -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
diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex
index cdc4254864..23f386b976 100644
--- a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex
+++ b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex
@@ -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
diff --git a/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs
new file mode 100644
index 0000000000..d1203de5e9
--- /dev/null
+++ b/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs
@@ -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
diff --git a/config/runtime.exs b/config/runtime.exs
index f00a02c4f9..1674d7f576 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -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")
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index 1cc45e89d0..71ec80526a 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -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