diff --git a/CHANGELOG.md b/CHANGELOG.md index c2029ecdeb..c7f982fc03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - [#5807](https://github.com/blockscout/blockscout/pull/5807) - Update Makefile migrate command due to release build - [#5786](https://github.com/blockscout/blockscout/pull/5786) - Replace `current_path` with `Controller.current_full_path` in two controllers - [#5948](https://github.com/blockscout/blockscout/pull/5948) - Fix unexpected messages in `CoinBalanceOnDemand` +- [#6013](https://github.com/blockscout/blockscout/pull/6013) - Fix ERC-1155 tokens fetching ### Chore diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex index 7350c31881..3aa9c61878 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex @@ -59,7 +59,7 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), {:ok, token_transfer} <- - Chain.erc721_token_instance_from_token_id_and_token_address(token_id, hash) do + Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do render( conn, "index.html", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex index 1696ac1339..d7d0a4d8c4 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Tokens.InstanceController do def show(conn, %{"token_id" => token_address_hash, "id" => token_id}) do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), :ok <- Chain.check_token_exists(hash), - :ok <- Chain.check_erc721_token_instance_exists(token_id, hash) do + :ok <- Chain.check_erc721_or_erc1155_token_instance_exists(token_id, hash) do token_instance_transfer_path = conn |> token_instance_transfer_path(:index, token_address_hash, token_id) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance/transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance/transfer_controller_test.exs new file mode 100644 index 0000000000..ee04094cc9 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance/transfer_controller_test.exs @@ -0,0 +1,44 @@ +defmodule BlockScoutWeb.Tokens.Instance.TransferControllerTest do + use BlockScoutWeb.ConnCase, async: false + + describe "GET token-transfers/2" do + test "works for ERC-721 tokens", %{conn: conn} do + contract_address = insert(:address) + + insert(:token, contract_address: contract_address) + + token_id = 10 + + %{log_index: log_index} = + insert(:token_transfer, + from_address: contract_address, + token_contract_address: contract_address, + token_id: token_id + ) + + conn = get(conn, "/token/#{contract_address.hash}/instance/#{token_id}/token-transfers") + + assert %{assigns: %{token_instance: %{log_index: ^log_index}}} = conn + end + + test "works for ERC-1155 tokens", %{conn: conn} do + contract_address = insert(:address) + + insert(:token, contract_address: contract_address) + + token_id = 10 + + %{log_index: log_index} = + insert(:token_transfer, + from_address: contract_address, + token_contract_address: contract_address, + token_id: nil, + token_ids: [token_id] + ) + + conn = get(conn, "/token/#{contract_address.hash}/instance/#{token_id}/token-transfers") + + assert %{assigns: %{token_instance: %{log_index: ^log_index}}} = conn + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs index 10d8831331..7ced36daf4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs @@ -19,5 +19,28 @@ defmodule BlockScoutWeb.Tokens.InstanceControllerTest do assert conn.status == 302 end + + test "works for ERC-1155 tokens", %{conn: conn} do + contract_address = insert(:address) + + insert(:token, contract_address: contract_address) + + token_id = 10 + + insert(:token_transfer, + from_address: contract_address, + token_contract_address: contract_address, + token_id: nil, + token_ids: [token_id] + ) + + conn = get(conn, token_instance_path(BlockScoutWeb.Endpoint, :show, to_string(contract_address.hash), token_id)) + + assert conn.status == 302 + + assert get_resp_header(conn, "location") == [ + "/token/#{contract_address.hash}/instance/#{token_id}/token-transfers" + ] + end end end diff --git a/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs index 764c982063..cc1c9f1297 100644 --- a/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs @@ -40,11 +40,11 @@ defmodule BlockScoutWeb.ApiDocsViewTest do describe "blockscout_url/2" do setup do - on_exit(fn -> - Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, - url: [scheme: "http", host: "localhost", api_path: nil, path: nil] - ) - end) + original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint) + + on_exit(fn -> Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, original) end) + + :ok end test "set_path = true returns url with path" do diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index ac9ff7acb6..8c121738bd 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -4881,6 +4881,28 @@ defmodule Explorer.Chain do end end + @spec erc721_or_erc1155_token_instance_from_token_id_and_token_address(binary(), Hash.Address.t()) :: + {:ok, TokenTransfer.t()} | {:error, :not_found} + def erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, token_contract_address) do + query = + from(tt in TokenTransfer, + left_join: instance in Instance, + on: + tt.token_contract_address_hash == instance.token_contract_address_hash and + (tt.token_id == instance.token_id or fragment("? @> ARRAY[?::decimal]", tt.token_ids, instance.token_id)), + where: + tt.token_contract_address_hash == ^token_contract_address and + (tt.token_id == ^token_id or fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id))), + limit: 1, + select: %{tt | instance: instance} + ) + + case Repo.one(query) do + nil -> {:error, :not_found} + token_instance -> {:ok, token_instance} + end + end + defp fetch_coin_balances(address_hash, paging_options) do address = Repo.get_by(Address, hash: address_hash) @@ -5505,7 +5527,7 @@ defmodule Explorer.Chain do end @doc """ - Checks if a `t:Explorer.Chain.TokenTransfer.t/0` with the given `hash` and `token_id` exists. + Checks if a `t:Explorer.Chain.TokenTransfer.t/0` of type ERC-721 with the given `hash` and `token_id` exists. Returns `:ok` if found @@ -5533,7 +5555,46 @@ defmodule Explorer.Chain do end @doc """ - Checks if a `t:Explorer.Chain.TokenTransfer.t/0` with the given `hash` and `token_id` exists. + Checks if a `t:Explorer.Chain.TokenTransfer.t/0` of type ERC-721 or ERC-1155 with the given `hash` and `token_id` exists. + + Returns `:ok` if found + + iex> contract_address = insert(:address) + iex> token_id = 10 + iex> insert(:token_transfer, + ...> from_address: contract_address, + ...> token_contract_address: contract_address, + ...> token_id: token_id + ...> ) + iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(token_id, contract_address.hash) + :ok + + iex> contract_address = insert(:address) + iex> token_id = 10 + iex> insert(:token_transfer, + ...> from_address: contract_address, + ...> token_contract_address: contract_address, + ...> token_ids: [token_id] + ...> ) + iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(token_id, contract_address.hash) + :ok + + Returns `:not_found` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(10, hash) + :not_found + """ + @spec check_erc721_or_erc1155_token_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) :: + :ok | :not_found + def check_erc721_or_erc1155_token_instance_exists(token_id, hash) do + token_id + |> erc721_or_erc1155_token_instance_exist?(hash) + |> boolean_to_check_result() + end + + @doc """ + Checks if a `t:Explorer.Chain.TokenTransfer.t/0` of type ERC-721 with the given `hash` and `token_id` exists. Returns `true` if found @@ -5563,6 +5624,49 @@ defmodule Explorer.Chain do Repo.exists?(query) end + @doc """ + Checks if a `t:Explorer.Chain.TokenTransfer.t/0` of type ERC-721 or ERC-1155 with the given `hash` and `token_id` exists. + + Returns `true` if found + + iex> contract_address = insert(:address) + iex> token_id = 10 + iex> insert(:token_transfer, + ...> from_address: contract_address, + ...> token_contract_address: contract_address, + ...> token_id: token_id + ...> ) + iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(token_id, contract_address.hash) + true + + iex> contract_address = insert(:address) + iex> token_id = 10 + iex> insert(:token_transfer, + ...> from_address: contract_address, + ...> token_contract_address: contract_address, + ...> token_ids: [token_id] + ...> ) + iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(token_id, contract_address.hash) + true + + Returns `false` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(10, hash) + false + """ + @spec erc721_or_erc1155_token_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean() + def erc721_or_erc1155_token_instance_exist?(token_id, hash) do + query = + from(tt in TokenTransfer, + where: + tt.token_contract_address_hash == ^hash and + (tt.token_id == ^token_id or fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id))) + ) + + Repo.exists?(query) + end + defp boolean_to_check_result(true), do: :ok defp boolean_to_check_result(false), do: :not_found diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index fe17971fb5..4c23267682 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -177,7 +177,7 @@ defmodule Explorer.Chain.TokenTransfer do from( tt in TokenTransfer, where: tt.token_contract_address_hash == ^token_address_hash, - where: tt.token_id == ^token_id, + where: tt.token_id == ^token_id or fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id)), where: not is_nil(tt.block_number), preload: [{:transaction, :block}, :token, :from_address, :to_address], order_by: [desc: tt.block_number] @@ -206,7 +206,9 @@ defmodule Explorer.Chain.TokenTransfer do query = from( tt in TokenTransfer, - where: tt.token_contract_address_hash == ^token_address_hash and tt.token_id == ^token_id, + where: + tt.token_contract_address_hash == ^token_address_hash and + (tt.token_id == ^token_id or fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id))), select: fragment("COUNT(*)") ) diff --git a/apps/explorer/priv/repo/migrations/20220902103213_create_index_token_transfers_token_ids.exs b/apps/explorer/priv/repo/migrations/20220902103213_create_index_token_transfers_token_ids.exs new file mode 100644 index 0000000000..e7f30e9cc0 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20220902103213_create_index_token_transfers_token_ids.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.Migrations.CreateIndexTokenTransfersTokenIds do + use Ecto.Migration + + def up do + execute(""" + CREATE INDEX token_transfers_token_ids_index on token_transfers USING GIN ("token_ids") + """) + end + + def down do + execute("DROP INDEX token_transfers_token_ids_index") + end +end diff --git a/apps/explorer/priv/repo/migrations/20220902103527_create_indexes_token_instances_token_contract_address_hash_token_id.exs b/apps/explorer/priv/repo/migrations/20220902103527_create_indexes_token_instances_token_contract_address_hash_token_id.exs new file mode 100644 index 0000000000..a91fc7d0fe --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20220902103527_create_indexes_token_instances_token_contract_address_hash_token_id.exs @@ -0,0 +1,8 @@ +defmodule Explorer.Repo.Migrations.CreateIndexesTokenInstancesTokenContractAddressHashTokenId do + use Ecto.Migration + + def change do + create_if_not_exists(index(:token_instances, [:token_contract_address_hash, :token_id])) + create_if_not_exists(index(:token_instances, [:token_id])) + end +end