diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b46de6f2..94f3b65130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Current ### Features +- [#4761](https://github.com/blockscout/blockscout/pull/4761) - ERC-1155 support - [#4739](https://github.com/blockscout/blockscout/pull/4739) - Improve logs and inputs decoding - [#4747](https://github.com/blockscout/blockscout/pull/4747) - Advanced CSV export - [#4745](https://github.com/blockscout/blockscout/pull/4745) - Vyper contracts verification diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex new file mode 100644 index 0000000000..0d97c56692 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex @@ -0,0 +1,77 @@ +defmodule BlockScoutWeb.Tokens.Instance.HolderController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.Tokens.HolderView + alias Explorer.{Chain, Market} + alias Explorer.Chain.Address + alias Phoenix.View + + 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, "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), + 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) + + next_page_path = + case next_page_params(next_page, token_holders_paginated, params) do + nil -> + nil + + next_page_params -> + token_instance_holder_path( + conn, + :index, + Address.checksum(token.contract_address_hash), + token_id, + Map.delete(next_page_params, "type") + ) + end + + holders_json = + token_holders_paginated + |> Enum.sort_by(& &1.value, &>=/2) + |> Enum.map(fn current_token_balance -> + View.render_to_string( + HolderView, + "_token_balances.html", + address_hash: address_hash, + token_balance: current_token_balance, + token: token + ) + end) + + json(conn, %{items: holders_json, next_page_path: next_page_path}) + else + _ -> + not_found(conn) + end + end + + def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id}) 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), + {:ok, token_transfer} <- + Chain.erc721_token_instance_from_token_id_and_token_address(token_id, hash) do + render( + conn, + "index.html", + token_instance: token_transfer, + current_path: current_path(conn), + token: Market.add_price(token), + total_token_transfers: Chain.count_token_transfers_from_token_hash_and_token_id(hash, token_id) + ) + else + _ -> + not_found(conn) + end + end + + def index(conn, _) do + not_found(conn) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex index 702dcff28e..6cd24629e8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex @@ -12,18 +12,18 @@ defmodule BlockScoutWeb.Tokens.TokenController do end def token_counters(conn, %{"id" => address_hash_string}) do - with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, token} <- Chain.token_from_address_hash(address_hash) do - {transfer_count, token_holder_count} = fetch_token_counters(token, address_hash, 200) + case Chain.string_to_address_hash(address_hash_string) do + {:ok, address_hash} -> + {transfer_count, token_holder_count} = fetch_token_counters(address_hash, 200) + + json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count}) - json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count}) - else _ -> not_found(conn) end end - defp fetch_token_counters(token, address_hash, timeout) do + defp fetch_token_counters(address_hash, timeout) do total_token_transfers_task = Task.async(fn -> TokenTransfersCounter.fetch(address_hash) @@ -31,7 +31,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do total_token_holders_task = Task.async(fn -> - token.holder_count || TokenHoldersCounter.fetch(address_hash) + TokenHoldersCounter.fetch(address_hash) end) [total_token_transfers_task, total_token_holders_task] diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex index 502df98049..ca2b3ad971 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex @@ -59,6 +59,15 @@ ) %> <% end %> + <%= if Enum.any?(@token_balances, fn {token_balance, _, _} -> token_balance.token.type == "ERC-1155" end) do %> + <%= render( + "_tokens.html", + conn: @conn, + token_balances: filter_by_type(@token_balances, "ERC-1155"), + type: "ERC-1155" + ) %> + <% end %> + <%= if Enum.any?(@token_balances, fn {token_balance, _, _} -> token_balance.token.type == "ERC-20" end) do %> <%= render( "_tokens.html", diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex index 3d3ea373b6..84033ccbd1 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex @@ -10,8 +10,14 @@ data-token-name="<%= token_name(token_balance.token) %>" data-token-symbol="<%= token_symbol(token_balance.token) %>" > + <% path = cond do + token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id)) + token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id)) + true -> token_path(@conn, :show, to_string(token_balance.token.contract_address_hash)) + end + %> <%= link( - to: token_path(@conn, :show, to_string(token.contract_address_hash)), + to: path, class: "dropdown-item" ) do %>
@@ -36,7 +42,14 @@
<% col_md = if token_balance.token.usd_value, do: "col-md-6", else: "col-md-12" %>

- <%= format_according_to_decimals(token_balance.value, token.decimals) %> <%= token_symbol(token) %> + <%= if token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) do %> + 1 + <% else %> + <%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_symbol(token_balance.token) %> + <% end %> + <%= if (token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id)) or token_balance.token_type == "ERC-1155" do %> + <%= " TokenID " <> to_string(token_balance.token_id) %> + <% end %>

<%= if token_balance.token.usd_value do %>

diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex new file mode 100644 index 0000000000..d79aff67ed --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex @@ -0,0 +1,12 @@ +<%= case @type do %> + <% :token_burning -> %> + <%= gettext("Token Burning") %> + <% :token_minting -> %> + <%= gettext("Token Minting") %> + <% :token_spawning -> %> + <%= gettext("Token Creation") %> + <% :token_transfer -> %> + <%= gettext("Token Transfer") %> + <% _ -> %> + <%= gettext("Token Transfer") %> +<% end %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex index 105207a70c..f8fecf59a6 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex @@ -7,7 +7,7 @@ - <%= format_token_balance_value(@token_balance.value, @token) %> <%= @token.symbol %> + <%= format_token_balance_value(@token_balance.value, @token_balance.token_id, @token) %> <%= @token.symbol %> <%= if show_total_supply_percentage?(@token.total_supply) do %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex new file mode 100644 index 0000000000..7fe594a3db --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex @@ -0,0 +1,41 @@ +

+ <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + token_id: @token_instance.token_id, + token_instance: @token_instance, + conn: @conn + ) %> + +
+
+ <%= render OverviewView, "_tabs.html", assigns %> +
+

<%= gettext "Token Holders" %>

+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ + + + + +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex index b046f97b8e..57badb7e4d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex @@ -5,11 +5,18 @@ to: token_instance_path(@conn, :show, @token.contract_address_hash, to_string(@token_instance.token_id)) ) %> - <%= if @token_instance.instance do %> + <%= if @token_instance.instance do %> <%= link( gettext("Metadata"), to: token_instance_metadata_path(@conn, :index, Address.checksum(@token.contract_address_hash), to_string(@token_instance.token_id)), class: "card-tab #{tab_status("metadata", @conn.request_path)}") %> <% end %> + <%= if !Chain.token_id_1155_is_unique?(@token.contract_address_hash, @token_instance.token_id) and @token.type == "ERC-1155" do %> + <%= link( + gettext("Token Holders"), + to: token_instance_holder_path(@conn, :index, Address.checksum(@token.contract_address_hash), to_string(@token_instance.token_id)), + class: "card-tab #{tab_status("token-holders", @conn.request_path)}") + %> + <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex index ae004ddeee..7db024cd0a 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex @@ -1,30 +1,46 @@ +<% is_1155 = @token.type == "ERC-1155"%> +<% is_unique = Chain.token_id_1155_is_unique?(@token.contract_address_hash, @token_transfer.token_id) or not is_1155%>
- <%= gettext "Unique Token" %> + <%= if is_unique do%> + <%= gettext "Unique Token" %> + <% else %> + <%= gettext "Not unique Token" %> + <% end %>
-
- - <%= gettext "Token ID" %>: - - <%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %> + <%= if is_unique do %> +
+ + <%= gettext "Token ID" %>: + + <%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %> + - - - - <%= gettext "Owner Address" %>: - - <%= render BlockScoutWeb.AddressView, - "_link.html", - address: @token_transfer.to_address, - contract: false, - use_custom_tooltip: false %> + + <%= gettext "Owner Address" %>: + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @token_transfer.to_address, + contract: false, + use_custom_tooltip: false %> + - -
+
+ <% else %> +
+ + <%= gettext "Token ID" %>: + + <%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %> + + +
+ <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex index dc2e48c014..d1d151ffe2 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex @@ -3,14 +3,7 @@
- <%= cond do %> - <% @token_transfer.to_address.hash == @burn_address_hash -> %> - <%= gettext("Token Burning") %> - <% @token_transfer.from_address.hash == @burn_address_hash -> %> - <%= gettext("Token Minting") %> - <% true -> %> - <%= gettext("Token Transfer") %> - <% end %> + <%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex new file mode 100644 index 0000000000..88273f4bc0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex @@ -0,0 +1 @@ +<%= "[" %><%= link(short_token_id(@token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@token_id)), "data-test": "token_link") %><%= "]" %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex new file mode 100644 index 0000000000..73cb4a2c2c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex @@ -0,0 +1 @@ +<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex index c0b0d09119..fd9efa4435 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex @@ -1,8 +1,23 @@ <%= case token_transfer_amount(@transfer) do %> <% {:ok, :erc721_instance} -> %> - <%= "TokenID ["%><%= link(short_token_id(@transfer.token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@transfer.token_id)), "data-test": "token_link") %><%= "]" %> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @transfer.token_id %> + <% {:ok, :erc1155_instance, value} -> %> + <% transfer_type = Chain.get_token_transfer_type(@transfer) %> + <%= if transfer_type == :token_spawning do %> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @transfer.token_id %> + <% else %> + <%= "#{value} " %> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @transfer.token_id %> + <% end %> + <% {:ok, :erc1155_instance, values, token_ids, _decimals} -> %> + <% values_ids = Enum.zip(values, token_ids) %> + <%= for {value, token_id} <- values_ids do %> +
+ <%= "#{value} "%> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: token_id %> +
+ <% end %> <% {:ok, value} -> %> <%= value %> -<% end %> -<%= " "%> -<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %> \ No newline at end of file + <%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %> +<% end %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex new file mode 100644 index 0000000000..44a3444aa1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex @@ -0,0 +1,2 @@ +<%= "TokenID " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_instance.html", transfer: @transfer, token_id: @token_id %> +<%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex index 3bbbe6a419..885990a377 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex @@ -1,14 +1,7 @@
- <%= cond do %> - <% @token_transfer.to_address.hash == @burn_address_hash -> %> - <%= gettext("Token Burning") %> - <% @token_transfer.from_address.hash == @burn_address_hash -> %> - <%= gettext("Token Minting") %> - <% true -> %> - <%= gettext("Token Transfer") %> - <% end %> + <%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 30f9803259..cdc8b8375a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -172,6 +172,12 @@ defmodule BlockScoutWeb.API.RPC.AddressView do |> Map.put_new(:tokenID, token_transfer.token_id) end + defp prepare_token_transfer(%{token_type: "ERC-1155"} = token_transfer) do + token_transfer + |> prepare_common_token_transfer() + |> Map.put_new(:tokenID, token_transfer.token_id) + end + defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do token_transfer |> prepare_common_token_transfer() diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex index 7652fa14ad..7b8d3276d6 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex @@ -16,27 +16,39 @@ defmodule BlockScoutWeb.Tokens.Helpers do When the token's type is ERC-721, the function will return a string with the token_id that represents the ERC-721 token since this kind of token doesn't have amount and decimals. """ + def token_transfer_amount(%{token: token, amount: amount, amounts: amounts, token_id: token_id, token_ids: token_ids}) do + do_token_transfer_amount(token, amount, amounts, token_id, token_ids) + end + def token_transfer_amount(%{token: token, amount: amount, token_id: token_id}) do - do_token_transfer_amount(token, amount, token_id) + do_token_transfer_amount(token, amount, nil, token_id, nil) end - defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, _token_id) do + defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, nil, _token_id, _token_ids) do {:ok, "--"} end - defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, amount, _token_id) do + defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, amount, _amounts, _token_id, _token_ids) do {:ok, CurrencyHelpers.format_according_to_decimals(amount, Decimal.new(0))} end - defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, amount, _token_id) do + defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, amount, _amounts, _token_id, _token_ids) do {:ok, CurrencyHelpers.format_according_to_decimals(amount, decimals)} end - defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, _token_id) do + defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, _amounts, _token_id, _token_ids) do {:ok, :erc721_instance} end - defp do_token_transfer_amount(_token, _amount, _token_id) do + defp do_token_transfer_amount(%Token{type: "ERC-1155", decimals: decimals}, amount, amounts, _token_id, token_ids) do + if amount do + {:ok, :erc1155_instance, CurrencyHelpers.format_according_to_decimals(amount, decimals)} + else + {:ok, :erc1155_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount(_token, _amount, _amounts, _token_id, _token_ids) do nil end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex index e185e3f9e8..2edfd93981 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex @@ -54,19 +54,23 @@ defmodule BlockScoutWeb.Tokens.HolderView do ## Examples iex> token = build(:token, type: "ERC-20", decimals: Decimal.new(2)) - iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, token) + iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, nil, token) "1,000" iex> token = build(:token, type: "ERC-721") - iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, token) + iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, nil, token) 1 """ - def format_token_balance_value(value, %Token{type: "ERC-20", decimals: decimals}) do + def format_token_balance_value(value, _id, %Token{type: "ERC-20", decimals: decimals}) do format_according_to_decimals(value, decimals) end - def format_token_balance_value(value, _token) do + def format_token_balance_value(value, id, %Token{type: "ERC-1155", decimals: decimals}) do + to_string(format_according_to_decimals(value, decimals)) <> " TokenID " <> to_string(id) + end + + def format_token_balance_value(value, _id, _token) do value end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex new file mode 100644 index 0000000000..38cf207bc4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tokens.Instance.HolderView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.Instance.OverviewView +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex index a539ef3d7f..2e396b2b50 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do use BlockScoutWeb, :view alias BlockScoutWeb.CurrencyHelpers + alias Explorer.Chain alias Explorer.Chain.{Address, SmartContract, Token} alias Explorer.SmartContract.Helper alias FileInfo diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex index e720e1b105..547d6dd33a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex @@ -4,4 +4,5 @@ defmodule BlockScoutWeb.Tokens.InventoryView do import BlockScoutWeb.Tokens.Instance.OverviewView, only: [media_src: 1, media_type: 1] alias BlockScoutWeb.Tokens.OverviewView + alias Explorer.Chain end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex index f18b68ee74..b2ac3a6832 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex @@ -44,6 +44,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do defp tab_name(["inventory"]), do: gettext("Inventory") def display_inventory?(%Token{type: "ERC-721"}), do: true + def display_inventory?(%Token{type: "ERC-1155"}), do: true def display_inventory?(_), do: false def smart_contract_with_read_only_functions?( diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex index 96f29c5fe0..3ea5d84840 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex @@ -2,5 +2,6 @@ defmodule BlockScoutWeb.Tokens.TransferView do use BlockScoutWeb, :view alias BlockScoutWeb.Tokens.OverviewView + alias Explorer.Chain alias Explorer.Chain.Address end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex index eb8fe81053..66999ad804 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex @@ -1,3 +1,5 @@ defmodule BlockScoutWeb.TransactionTokenTransferView do use BlockScoutWeb, :view + + alias Explorer.Chain end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 84856e8af9..afe6fe67dc 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -184,33 +184,24 @@ defmodule BlockScoutWeb.TransactionView do end) new_acc1 = - if token_transfer.token_id do - [new_entry | acc1] - else - if existing_entry do - acc1 - |> Enum.map(fn entry -> - if entry.to_address_hash == token_transfer.to_address_hash && - entry.from_address_hash == token_transfer.from_address_hash && - entry.token == token_transfer.token do - updated_entry = - if new_entry.amount do - %{ - entry - | amount: Decimal.add(new_entry.amount, entry.amount) - } - else - entry - end - - updated_entry - else + if existing_entry do + acc1 + |> Enum.map(fn entry -> + if entry.to_address_hash == token_transfer.to_address_hash && + entry.from_address_hash == token_transfer.from_address_hash && + entry.token == token_transfer.token do + updated_entry = %{ entry - end - end) - else - [new_entry | acc1] - end + | amount: Decimal.add(new_entry.amount, entry.amount) + } + + updated_entry + else + entry + end + end) + else + [new_entry | acc1] end {new_acc1, acc2} @@ -220,6 +211,7 @@ defmodule BlockScoutWeb.TransactionView do case type do :erc20 -> gettext("ERC-20 ") :erc721 -> gettext("ERC-721 ") + :erc1155 -> gettext("ERC-1155 ") _ -> "" end end diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index b0b7ad9998..602dea1bfe 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex @@ -267,6 +267,13 @@ defmodule BlockScoutWeb.WebRouter do only: [:index], as: :metadata ) + + resources( + "/token-holders", + Tokens.Instance.HolderController, + only: [:index], + as: :holder + ) end end @@ -318,6 +325,13 @@ defmodule BlockScoutWeb.WebRouter do only: [:index], as: :metadata ) + + resources( + "/token-holders", + Tokens.Instance.HolderController, + only: [:index], + as: :holder + ) end end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index ea952ce7c8..ac18805dc5 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -81,7 +81,7 @@ msgid "%{subnetwork} Staking DApp - BlockScout" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:356 +#: lib/block_scout_web/views/transaction_view.ex:348 msgid "(Awaiting internal transactions for status)" msgstr "" @@ -348,7 +348,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/block/_link.html.eex:2 -#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:50 +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 msgid "Block #%{number}" msgstr "" @@ -580,7 +580,7 @@ msgid "Compiler version" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:349 +#: lib/block_scout_web/views/transaction_view.ex:341 msgid "Confirmed" msgstr "" @@ -654,12 +654,12 @@ msgid "Contract Address Pending" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:460 +#: lib/block_scout_web/views/transaction_view.ex:452 msgid "Contract Call" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:457 +#: lib/block_scout_web/views/transaction_view.ex:449 msgid "Contract Creation" msgstr "" @@ -987,12 +987,12 @@ msgid "EIP-1167" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:221 +#: lib/block_scout_web/views/transaction_view.ex:212 msgid "ERC-20 " msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:222 +#: lib/block_scout_web/views/transaction_view.ex:213 msgid "ERC-721 " msgstr "" @@ -1061,12 +1061,12 @@ msgid "Error trying to fetch balances." msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:360 +#: lib/block_scout_web/views/transaction_view.ex:352 msgid "Error: %{reason}" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:358 +#: lib/block_scout_web/views/transaction_view.ex:350 msgid "Error: (Awaiting internal transactions for reason)" msgstr "" @@ -1313,7 +1313,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:28 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:21 lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 lib/block_scout_web/views/address_view.ex:346 -#: lib/block_scout_web/views/transaction_view.ex:515 +#: lib/block_scout_web/views/transaction_view.ex:507 msgid "Internal Transactions" msgstr "" @@ -1440,7 +1440,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:41 #: lib/block_scout_web/templates/address_logs/index.html.eex:10 lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 lib/block_scout_web/views/address_view.ex:357 -#: lib/block_scout_web/views/transaction_view.ex:516 +#: lib/block_scout_web/views/transaction_view.ex:508 msgid "Logs" msgstr "" @@ -1481,7 +1481,7 @@ msgid "Max Priority Fee per Gas" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:330 +#: lib/block_scout_web/views/transaction_view.ex:322 msgid "Max of" msgstr "" @@ -1497,7 +1497,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 -#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:178 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:179 msgid "Metadata" msgstr "" @@ -1684,7 +1684,7 @@ msgid "Other Explorers" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 msgid "Owner Address" msgstr "" @@ -1712,8 +1712,8 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/layout/_topnav.html.eex:53 -#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:355 -#: lib/block_scout_web/views/transaction_view.ex:389 +#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:347 +#: lib/block_scout_web/views/transaction_view.ex:381 msgid "Pending" msgstr "" @@ -1831,7 +1831,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 -#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:517 +#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:509 msgid "Raw Trace" msgstr "" @@ -2037,9 +2037,9 @@ msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:54 lib/block_scout_web/templates/address_validation/index.html.eex:24 #: lib/block_scout_web/templates/block_transaction/index.html.eex:22 lib/block_scout_web/templates/chain/show.html.eex:180 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 lib/block_scout_web/templates/stakes/_table.html.eex:49 -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 -#: lib/block_scout_web/templates/transaction/index.html.eex:29 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 lib/block_scout_web/templates/transaction/index.html.eex:29 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 lib/block_scout_web/templates/transaction_log/index.html.eex:15 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 msgid "Something went wrong, click to reload." @@ -2144,7 +2144,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 -#: lib/block_scout_web/views/transaction_view.ex:357 +#: lib/block_scout_web/views/transaction_view.ex:349 msgid "Success" msgstr "" @@ -2365,8 +2365,8 @@ msgid "There are no transactions." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 lib/block_scout_web/templates/tokens/transfer/index.html.eex:26 msgid "There are no transfers for this Token." msgstr "" @@ -2477,13 +2477,12 @@ msgid "Token" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:8 -#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:6 lib/block_scout_web/views/transaction_view.ex:451 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 lib/block_scout_web/views/transaction_view.ex:443 msgid "Token Burning" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:452 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 lib/block_scout_web/views/transaction_view.ex:444 msgid "Token Creation" msgstr "" @@ -2495,25 +2494,25 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/holder/index.html.eex:20 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:9 lib/block_scout_web/views/tokens/overview_view.ex:42 msgid "Token Holders" msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 -#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 msgid "Token ID" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:10 -#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:8 lib/block_scout_web/views/transaction_view.ex:450 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 lib/block_scout_web/views/transaction_view.ex:442 msgid "Token Minting" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:12 -#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:10 lib/block_scout_web/views/transaction_view.ex:453 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 lib/block_scout_web/views/transaction_view.ex:445 msgid "Token Transfer" msgstr "" @@ -2523,8 +2522,8 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:3 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 lib/block_scout_web/views/address_view.ex:348 -#: lib/block_scout_web/views/tokens/instance/overview_view.ex:177 lib/block_scout_web/views/tokens/overview_view.ex:41 -#: lib/block_scout_web/views/transaction_view.ex:514 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:178 lib/block_scout_web/views/tokens/overview_view.ex:41 +#: lib/block_scout_web/views/transaction_view.ex:506 msgid "Token Transfers" msgstr "" @@ -2620,7 +2619,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 -#: lib/block_scout_web/views/transaction_view.ex:463 +#: lib/block_scout_web/views/transaction_view.ex:455 msgid "Transaction" msgstr "" @@ -2750,12 +2749,12 @@ msgid "Uncles" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:348 +#: lib/block_scout_web/views/transaction_view.ex:340 msgid "Unconfirmed" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:6 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 msgid "Unique Token" msgstr "" @@ -3191,3 +3190,13 @@ msgstr "" #: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:18 msgid "validator" msgstr "" + +#, elixir-format +#: lib/block_scout_web/views/transaction_view.ex:214 +msgid "ERC-1155 " +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 +msgid "Not unique Token" +msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index ea952ce7c8..ac18805dc5 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -81,7 +81,7 @@ msgid "%{subnetwork} Staking DApp - BlockScout" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:356 +#: lib/block_scout_web/views/transaction_view.ex:348 msgid "(Awaiting internal transactions for status)" msgstr "" @@ -348,7 +348,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/block/_link.html.eex:2 -#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:50 +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 msgid "Block #%{number}" msgstr "" @@ -580,7 +580,7 @@ msgid "Compiler version" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:349 +#: lib/block_scout_web/views/transaction_view.ex:341 msgid "Confirmed" msgstr "" @@ -654,12 +654,12 @@ msgid "Contract Address Pending" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:460 +#: lib/block_scout_web/views/transaction_view.ex:452 msgid "Contract Call" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:457 +#: lib/block_scout_web/views/transaction_view.ex:449 msgid "Contract Creation" msgstr "" @@ -987,12 +987,12 @@ msgid "EIP-1167" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:221 +#: lib/block_scout_web/views/transaction_view.ex:212 msgid "ERC-20 " msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:222 +#: lib/block_scout_web/views/transaction_view.ex:213 msgid "ERC-721 " msgstr "" @@ -1061,12 +1061,12 @@ msgid "Error trying to fetch balances." msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:360 +#: lib/block_scout_web/views/transaction_view.ex:352 msgid "Error: %{reason}" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:358 +#: lib/block_scout_web/views/transaction_view.ex:350 msgid "Error: (Awaiting internal transactions for reason)" msgstr "" @@ -1313,7 +1313,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:28 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:21 lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 lib/block_scout_web/views/address_view.ex:346 -#: lib/block_scout_web/views/transaction_view.ex:515 +#: lib/block_scout_web/views/transaction_view.ex:507 msgid "Internal Transactions" msgstr "" @@ -1440,7 +1440,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:41 #: lib/block_scout_web/templates/address_logs/index.html.eex:10 lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 lib/block_scout_web/views/address_view.ex:357 -#: lib/block_scout_web/views/transaction_view.ex:516 +#: lib/block_scout_web/views/transaction_view.ex:508 msgid "Logs" msgstr "" @@ -1481,7 +1481,7 @@ msgid "Max Priority Fee per Gas" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:330 +#: lib/block_scout_web/views/transaction_view.ex:322 msgid "Max of" msgstr "" @@ -1497,7 +1497,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 -#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:178 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:179 msgid "Metadata" msgstr "" @@ -1684,7 +1684,7 @@ msgid "Other Explorers" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 msgid "Owner Address" msgstr "" @@ -1712,8 +1712,8 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/layout/_topnav.html.eex:53 -#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:355 -#: lib/block_scout_web/views/transaction_view.ex:389 +#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:347 +#: lib/block_scout_web/views/transaction_view.ex:381 msgid "Pending" msgstr "" @@ -1831,7 +1831,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 -#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:517 +#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:509 msgid "Raw Trace" msgstr "" @@ -2037,9 +2037,9 @@ msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:54 lib/block_scout_web/templates/address_validation/index.html.eex:24 #: lib/block_scout_web/templates/block_transaction/index.html.eex:22 lib/block_scout_web/templates/chain/show.html.eex:180 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 lib/block_scout_web/templates/stakes/_table.html.eex:49 -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 -#: lib/block_scout_web/templates/transaction/index.html.eex:29 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 lib/block_scout_web/templates/transaction/index.html.eex:29 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 lib/block_scout_web/templates/transaction_log/index.html.eex:15 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 msgid "Something went wrong, click to reload." @@ -2144,7 +2144,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 -#: lib/block_scout_web/views/transaction_view.ex:357 +#: lib/block_scout_web/views/transaction_view.ex:349 msgid "Success" msgstr "" @@ -2365,8 +2365,8 @@ msgid "There are no transactions." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 lib/block_scout_web/templates/tokens/transfer/index.html.eex:26 msgid "There are no transfers for this Token." msgstr "" @@ -2477,13 +2477,12 @@ msgid "Token" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:8 -#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:6 lib/block_scout_web/views/transaction_view.ex:451 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 lib/block_scout_web/views/transaction_view.ex:443 msgid "Token Burning" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:452 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 lib/block_scout_web/views/transaction_view.ex:444 msgid "Token Creation" msgstr "" @@ -2495,25 +2494,25 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/holder/index.html.eex:20 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:9 lib/block_scout_web/views/tokens/overview_view.ex:42 msgid "Token Holders" msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 -#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 msgid "Token ID" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:10 -#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:8 lib/block_scout_web/views/transaction_view.ex:450 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 lib/block_scout_web/views/transaction_view.ex:442 msgid "Token Minting" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:12 -#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:10 lib/block_scout_web/views/transaction_view.ex:453 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 lib/block_scout_web/views/transaction_view.ex:445 msgid "Token Transfer" msgstr "" @@ -2523,8 +2522,8 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:3 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 lib/block_scout_web/views/address_view.ex:348 -#: lib/block_scout_web/views/tokens/instance/overview_view.ex:177 lib/block_scout_web/views/tokens/overview_view.ex:41 -#: lib/block_scout_web/views/transaction_view.ex:514 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:178 lib/block_scout_web/views/tokens/overview_view.ex:41 +#: lib/block_scout_web/views/transaction_view.ex:506 msgid "Token Transfers" msgstr "" @@ -2620,7 +2619,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 -#: lib/block_scout_web/views/transaction_view.ex:463 +#: lib/block_scout_web/views/transaction_view.ex:455 msgid "Transaction" msgstr "" @@ -2750,12 +2749,12 @@ msgid "Uncles" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:348 +#: lib/block_scout_web/views/transaction_view.ex:340 msgid "Unconfirmed" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:6 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 msgid "Unique Token" msgstr "" @@ -3191,3 +3190,13 @@ msgstr "" #: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:18 msgid "validator" msgstr "" + +#, elixir-format +#: lib/block_scout_web/views/transaction_view.ex:214 +msgid "ERC-1155 " +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 +msgid "Not unique Token" +msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs index 20bd85e7ef..6b5c6c853d 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs @@ -56,19 +56,19 @@ defmodule BlockScoutWeb.Tokens.HolderViewTest do end end - describe "format_token_balance_value/1" do + describe "format_token_balance_value/3" do test "formats according to token decimals when it's a ERC-20" do token = build(:token, type: "ERC-20", decimals: Decimal.new(2)) token_balance = build(:token_balance, value: 2_000_000) - assert HolderView.format_token_balance_value(token_balance.value, token) == "20,000" + assert HolderView.format_token_balance_value(token_balance.value, nil, token) == "20,000" end test "returns the value when it's ERC-721" do token = build(:token, type: "ERC-721") token_balance = build(:token_balance, value: 1) - assert HolderView.format_token_balance_value(token_balance.value, token) == 1 + assert HolderView.format_token_balance_value(token_balance.value, nil, token) == 1 end end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index a772f615c5..15e9c843c0 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -168,7 +168,11 @@ defmodule EthereumJSONRPC do """ @spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()] def execute_contract_functions(functions, abi, json_rpc_named_arguments) do - Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments) + if Enum.count(functions) > 0 do + Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments) + else + [] + end end @doc """ diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index d6ad755f3b..33426b98a9 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -4539,7 +4539,7 @@ defmodule Explorer.Chain do nft_tokens = from( token in Token, - where: token.type == ^"ERC-721", + where: token.type == ^"ERC-721" or token.type == ^"ERC-1155", select: token.contract_address_hash ) @@ -5693,6 +5693,13 @@ defmodule Explorer.Chain do |> Repo.one() || Decimal.new(0) end + # @spec fetch_last_token_balance_1155(Hash.Address.t(), Hash.Address.t()) :: Decimal.t() + def fetch_last_token_balance_1155(address_hash, token_contract_address_hash, token_id) do + address_hash + |> CurrentTokenBalance.last_token_balance_1155(token_contract_address_hash, token_id) + |> Repo.one() || Decimal.new(0) + end + @spec address_to_coin_balances(Hash.Address.t(), [paging_options]) :: [] def address_to_coin_balances(address_hash, options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) @@ -5813,9 +5820,36 @@ defmodule Explorer.Chain do |> Repo.all() end + def fetch_token_holders_from_token_hash_and_token_id(contract_address_hash, token_id, options \\ []) do + contract_address_hash + |> CurrentTokenBalance.token_holders_1155_by_token_id(token_id, options) + |> Repo.all() + end + + def token_id_1155_is_unique?(_, nil), do: false + + def token_id_1155_is_unique?(contract_address_hash, token_id) do + result = contract_address_hash |> CurrentTokenBalance.token_balances_by_id_limit_2(token_id) |> Repo.all() + + if length(result) == 1 do + Decimal.cmp(Enum.at(result, 0), 1) == :eq + else + false + end + end + + def get_token_ids_1155(contract_address_hash) do + contract_address_hash + |> CurrentTokenBalance.token_ids_query() + |> Repo.all() + end + @spec count_token_holders_from_token_hash(Hash.Address.t()) :: non_neg_integer() def count_token_holders_from_token_hash(contract_address_hash) do - query = from(ctb in CurrentTokenBalance.token_holders_query(contract_address_hash), select: fragment("COUNT(*)")) + query = + from(ctb in CurrentTokenBalance.token_holders_query_for_count(contract_address_hash), + select: fragment("COUNT(DISTINCT(address_hash))") + ) Repo.one!(query, timeout: :infinity) end @@ -5853,7 +5887,7 @@ defmodule Explorer.Chain do end @spec transaction_token_transfer_type(Transaction.t()) :: - :erc20 | :erc721 | :token_transfer | nil + :erc20 | :erc721 | :erc1155 | :token_transfer | nil def transaction_token_transfer_type( %Transaction{ status: :ok, @@ -5900,10 +5934,24 @@ defmodule Explorer.Chain do find_erc721_token_transfer(transaction.token_transfers, {from_address, to_address}) + # safeTransferFrom(address,address,uint256,uint256,bytes) + {"0xf242432a" <> params, ^zero_wei} -> + types = [:address, :address, {:uint, 256}, {:uint, 256}, :bytes] + [from_address, to_address, _id, _value, _data] = decode_params(params, types) + + find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address}) + + # safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) + {"0x2eb2c2d6" <> params, ^zero_wei} -> + types = [:address, :address, [{:uint, 256}], [{:uint, 256}], :bytes] + [from_address, to_address, _ids, _values, _data] = decode_params(params, types) + + find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address}) + {"0xf907fc5b" <> _params, ^zero_wei} -> :erc20 - # check for ERC 20 or for old ERC 721 token versions + # check for ERC-20 or for old ERC-721, ERC-1155 token versions {unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} -> types = [:address, {:uint, 256}] @@ -5911,7 +5959,7 @@ defmodule Explorer.Chain do decimal_value = Decimal.new(value) - find_erc721_or_erc20_token_transfer(transaction.token_transfers, {address, decimal_value}) + find_erc721_or_erc20_or_erc1155_token_transfer(transaction.token_transfers, {address, decimal_value}) _ -> nil @@ -5927,7 +5975,16 @@ defmodule Explorer.Chain do if token_transfer, do: :erc721 end - defp find_erc721_or_erc20_token_transfer(token_transfers, {address, decimal_value}) do + defp find_erc1155_token_transfer(token_transfers, {from_address, to_address}) do + token_transfer = + Enum.find(token_transfers, fn token_transfer -> + token_transfer.from_address_hash.bytes == from_address && token_transfer.to_address_hash.bytes == to_address + end) + + if token_transfer, do: :erc1155 + end + + defp find_erc721_or_erc20_or_erc1155_token_transfer(token_transfers, {address, decimal_value}) do token_transfer = Enum.find(token_transfers, fn token_transfer -> token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value @@ -5937,6 +5994,7 @@ defmodule Explorer.Chain do case token_transfer.token do %Token{type: "ERC-20"} -> :erc20 %Token{type: "ERC-721"} -> :erc721 + %Token{type: "ERC-1155"} -> :erc1155 _ -> nil end else diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index db27975585..58d0b7a193 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -23,6 +23,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do * `token_contract_address_hash` - The contract address hash foreign key. * `block_number` - The block's number that the transfer took place. * `value` - The value that's represents the balance. + * `token_id` - The token_id of the transferred token (applicable for ERC-1155 and ERC-721 tokens) + * `token_type` - The type of the token """ @type t :: %__MODULE__{ address: %Ecto.Association.NotLoaded{} | Address.t(), @@ -30,15 +32,21 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do token: %Ecto.Association.NotLoaded{} | Token.t(), token_contract_address_hash: Hash.Address, block_number: Block.block_number(), + max_block_number: Block.block_number(), inserted_at: DateTime.t(), updated_at: DateTime.t(), - value: Decimal.t() | nil + value: Decimal.t() | nil, + token_id: non_neg_integer() | nil, + token_type: String.t() } schema "address_current_token_balances" do field(:value, :decimal) field(:block_number, :integer) + field(:max_block_number, :integer, virtual: true) field(:value_fetched_at, :utc_datetime_usec) + field(:token_id, :decimal) + field(:token_type, :string) # A transient field for deriving token holder count deltas during address_current_token_balances upserts field(:old_value, :decimal) @@ -56,8 +64,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do timestamps() end - @optional_fields ~w(value value_fetched_at)a - @required_fields ~w(address_hash block_number token_contract_address_hash)a + @optional_fields ~w(value value_fetched_at token_id)a + @required_fields ~w(address_hash block_number token_contract_address_hash token_type)a @allowed_fields @optional_fields ++ @required_fields @doc false @@ -96,6 +104,57 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do |> offset(^offset) end + @doc """ + Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash and token_id. + + The Token Holders are the addresses that own a positive amount of the Token. So this query is + considering the following conditions: + + * The token balance from the last block. + * Balances greater than 0. + * Excluding the burn address (0x0000000000000000000000000000000000000000). + + """ + def token_holders_1155_by_token_id(token_contract_address_hash, token_id, options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size + + token_contract_address_hash + |> token_holders_by_token_id_query(token_id) + |> preload(:address) + |> order_by([tb], desc: :value, desc: :address_hash) + |> Chain.page_token_balances(paging_options) + |> limit(^paging_options.page_size) + |> offset(^offset) + end + + @doc """ + Builds an `Ecto.Query` to fetch all available token_ids + """ + def token_ids_query(token_contract_address_hash) do + from( + ctb in __MODULE__, + where: ctb.token_contract_address_hash == ^token_contract_address_hash, + where: ctb.address_hash != ^@burn_address_hash, + where: ctb.value > 0, + select: ctb.token_id, + distinct: ctb.token_id + ) + end + + @doc """ + Builds an `Ecto.Query` to fetch all token holders, to count it + Used in `Explorer.Chain.count_token_holders_from_token_hash/1` + """ + def token_holders_query_for_count(token_contract_address_hash) do + from( + ctb in __MODULE__, + where: ctb.token_contract_address_hash == ^token_contract_address_hash, + where: ctb.address_hash != ^@burn_address_hash, + where: ctb.value > 0 + ) + end + @doc """ Builds an `t:Ecto.Query.t/0` to fetch the current token balances of the given address. """ @@ -137,6 +196,48 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do ) end + @doc """ + Builds an `t:Ecto.Query.t/0` to fetch the current balance of the given address for the given token and token_id + """ + def last_token_balance_1155(address_hash, token_contract_address_hash, token_id) do + from( + ctb in __MODULE__, + where: ctb.token_contract_address_hash == ^token_contract_address_hash, + where: ctb.address_hash == ^address_hash, + where: ctb.token_id == ^token_id, + select: ctb.value + ) + end + + @doc """ + Builds an `t:Ecto.Query.t/0` to check if the token_id corresponds to the unique token or not. + Used in `Explorer.Chain.token_id_1155_is_unique?/2` + """ + def token_balances_by_id_limit_2(token_contract_address_hash, token_id) do + from( + ctb in __MODULE__, + where: ctb.token_contract_address_hash == ^token_contract_address_hash, + where: ctb.token_id == ^token_id, + where: ctb.address_hash != ^@burn_address_hash, + where: ctb.value > 0, + select: ctb.value, + limit: 2 + ) + end + + @doc """ + Builds an `t:Ecto.Query.t/0` to fetch holders of the particular token_id in ERC-1155 + """ + def token_holders_by_token_id_query(token_contract_address_hash, token_id) do + from( + ctb in __MODULE__, + where: ctb.token_contract_address_hash == ^token_contract_address_hash, + where: ctb.address_hash != ^@burn_address_hash, + where: ctb.value > 0, + where: ctb.token_id == ^token_id + ) + end + @doc """ Builds an `t:Ecto.Query.t/0` to fetch addresses that hold the token. diff --git a/apps/explorer/lib/explorer/chain/address/token.ex b/apps/explorer/lib/explorer/chain/address/token.ex index 1dd167b779..b2bfabeaf2 100644 --- a/apps/explorer/lib/explorer/chain/address/token.ex +++ b/apps/explorer/lib/explorer/chain/address/token.ex @@ -34,17 +34,16 @@ defmodule Explorer.Chain.Address.Token do address_hash |> join_with_last_balance() - |> order_filter_and_group() + |> filter_and_group() + |> order() |> page_tokens(paging_options) |> limit(^paging_options.page_size) end - defp order_filter_and_group(query) do + defp filter_and_group(query) do from( [token, balance] in query, - order_by: fragment("? DESC, LOWER(?) ASC NULLS LAST", token.type, token.name), where: balance.value > 0, - group_by: [token.name, token.symbol, balance.value, token.type, token.contract_address_hash], select: %Address.Token{ contract_address_hash: token.contract_address_hash, inserted_at: max(token.inserted_at), @@ -53,22 +52,40 @@ defmodule Explorer.Chain.Address.Token do balance: balance.value, decimals: max(token.decimals), type: token.type - } + }, + group_by: [token.name, token.symbol, balance.value, token.type, token.contract_address_hash, balance.block_number] + ) + end + + defp order(query) do + from( + token in subquery(query), + order_by: fragment("? DESC, ? ASC NULLS LAST", token.type, token.name) ) end defp join_with_last_balance(address_hash) do last_balance_query = from( - tb in CurrentTokenBalance, - where: tb.address_hash == ^address_hash, - select: %{value: tb.value, token_contract_address_hash: tb.token_contract_address_hash} + ctb in CurrentTokenBalance, + where: ctb.address_hash == ^address_hash, + select: %{ + value: ctb.value, + token_contract_address_hash: ctb.token_contract_address_hash, + block_number: ctb.block_number, + max_block_number: over(max(ctb.block_number), :w) + }, + windows: [ + w: [partition_by: [ctb.token_contract_address_hash, ctb.address_hash]] + ] ) from( t in Chain.Token, join: tb in subquery(last_balance_query), - on: tb.token_contract_address_hash == t.contract_address_hash + on: tb.token_contract_address_hash == t.contract_address_hash, + where: tb.block_number == tb.max_block_number, + distinct: t.contract_address_hash ) end diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index d3941fd9e2..cc0d579da1 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -23,6 +23,8 @@ defmodule Explorer.Chain.Address.TokenBalance do * `token_contract_address_hash` - The contract address hash foreign key. * `block_number` - The block's number that the transfer took place. * `value` - The value that's represents the balance. + * `token_id` - The token_id of the transferred token (applicable for ERC-1155 and ERC-721 tokens) + * `token_type` - The type of the token """ @type t :: %__MODULE__{ address: %Ecto.Association.NotLoaded{} | Address.t(), @@ -32,13 +34,17 @@ defmodule Explorer.Chain.Address.TokenBalance do block_number: Block.block_number(), inserted_at: DateTime.t(), updated_at: DateTime.t(), - value: Decimal.t() | nil + value: Decimal.t() | nil, + token_id: non_neg_integer() | nil, + token_type: String.t() } schema "address_token_balances" do field(:value, :decimal) field(:block_number, :integer) field(:value_fetched_at, :utc_datetime_usec) + field(:token_id, :decimal) + field(:token_type, :string) belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) @@ -53,8 +59,8 @@ defmodule Explorer.Chain.Address.TokenBalance do timestamps() end - @optional_fields ~w(value value_fetched_at)a - @required_fields ~w(address_hash block_number token_contract_address_hash)a + @optional_fields ~w(value value_fetched_at token_id)a + @required_fields ~w(address_hash block_number token_contract_address_hash token_type)a @allowed_fields @optional_fields ++ @required_fields @doc false @@ -82,8 +88,9 @@ defmodule Explorer.Chain.Address.TokenBalance do tb in TokenBalance, join: t in Token, on: tb.token_contract_address_hash == t.contract_address_hash, - where: is_nil(tb.value_fetched_at) or is_nil(tb.value), - where: (tb.address_hash != ^@burn_address_hash and t.type != "ERC-721") or t.type == "ERC-20" + where: + ((tb.address_hash != ^@burn_address_hash and t.type != "ERC-721") or t.type == "ERC-20" or t.type == "ERC-1155") and + (is_nil(tb.value_fetched_at) or is_nil(tb.value)) ) end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex index ce3a22a365..a04cd71932 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex @@ -109,8 +109,16 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do # Enforce ShareLocks tables order (see docs: sharelocks.md) multi |> Multi.run(:acquire_contract_address_tokens, fn repo, _ -> - contract_address_hashes = changes_list |> Enum.map(& &1.token_contract_address_hash) |> Enum.uniq() - Tokens.acquire_contract_address_tokens(repo, contract_address_hashes) + token_contract_address_hashes_and_ids = + changes_list + |> Enum.map(fn change -> + token_id = get_tokend_id(change) + + {change.token_contract_address_hash, token_id} + end) + |> Enum.uniq() + + Tokens.acquire_contract_address_tokens(repo, token_contract_address_hashes_and_ids) end) |> Multi.run(:address_current_token_balances, fn repo, _ -> insert(repo, changes_list, insert_options) @@ -131,6 +139,10 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do end) end + defp get_tokend_id(change) do + if Map.has_key?(change, :token_id), do: change.token_id, else: nil + end + @impl Import.Runner def timeout, do: @timeout @@ -198,21 +210,107 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do | {:error, [Changeset.t()]} defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_atom(repo) and is_list(changes_list) do + inserted_changes_list = + insert_changes_list_with_and_without_token_id(changes_list, repo, timestamps, timeout, options) + + {:ok, inserted_changes_list} + end + + def insert_changes_list_with_and_without_token_id(changes_list, repo, timestamps, timeout, options) do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md) - ordered_changes_list = Enum.sort_by(changes_list, &{&1.token_contract_address_hash, &1.address_hash}) - - Import.insert_changes_list( - repo, - ordered_changes_list, - conflict_target: ~w(address_hash token_contract_address_hash)a, - on_conflict: on_conflict, - for: CurrentTokenBalance, - returning: true, - timeout: timeout, - timestamps: timestamps - ) + %{ + changes_list_no_token_id: changes_list_no_token_id, + changes_list_with_token_id: changes_list_with_token_id + } = + changes_list + |> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc -> + updated_change = + if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do + change + else + Map.put(change, :token_id, nil) + end + + if updated_change.token_id do + changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id] + + %{ + changes_list_no_token_id: acc.changes_list_no_token_id, + changes_list_with_token_id: changes_list_with_token_id + } + else + changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id] + + %{ + changes_list_no_token_id: changes_list_no_token_id, + changes_list_with_token_id: acc.changes_list_with_token_id + } + end + end) + + ordered_changes_list_no_token_id = + changes_list_no_token_id + |> Enum.group_by(fn %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash + } -> + {address_hash, token_contract_address_hash} + end) + |> Enum.map(fn {_, grouped_address_token_balances} -> + Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end) + end) + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash}) + + ordered_changes_list_with_token_id = + changes_list_with_token_id + |> Enum.group_by(fn %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id + } -> + {address_hash, token_contract_address_hash, token_id} + end) + |> Enum.map(fn {_, grouped_address_token_balances} -> + Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end) + end) + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash}) + + {:ok, inserted_changes_list_no_token_id} = + if Enum.count(ordered_changes_list_no_token_id) > 0 do + Import.insert_changes_list( + repo, + ordered_changes_list_no_token_id, + conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash) WHERE token_id IS NULL>}, + on_conflict: on_conflict, + for: CurrentTokenBalance, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + else + {:ok, []} + end + + {:ok, inserted_changes_list_with_token_id} = + if Enum.count(ordered_changes_list_with_token_id) > 0 do + Import.insert_changes_list( + repo, + ordered_changes_list_with_token_id, + conflict_target: + {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, token_id) WHERE token_id IS NOT NULL>}, + on_conflict: on_conflict, + for: CurrentTokenBalance, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + else + {:ok, []} + end + + inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id end defp default_on_conflict do diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex index 8189a5845c..66c3b3da88 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex @@ -60,20 +60,113 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce TokenBalance ShareLocks order (see docs: sharelocks.md) - ordered_changes_list = - Enum.sort_by(changes_list, &{&1.token_contract_address_hash, &1.address_hash, &1.block_number}) - - {:ok, _} = - Import.insert_changes_list( - repo, - ordered_changes_list, - conflict_target: ~w(address_hash token_contract_address_hash block_number)a, - on_conflict: on_conflict, - for: TokenBalance, - returning: true, - timeout: timeout, - timestamps: timestamps - ) + %{ + changes_list_no_token_id: changes_list_no_token_id, + changes_list_with_token_id: changes_list_with_token_id + } = + changes_list + |> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc -> + updated_change = + if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do + change + else + Map.put(change, :token_id, nil) + end + + if updated_change.token_id do + changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id] + + %{ + changes_list_no_token_id: acc.changes_list_no_token_id, + changes_list_with_token_id: changes_list_with_token_id + } + else + changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id] + + %{ + changes_list_no_token_id: changes_list_no_token_id, + changes_list_with_token_id: acc.changes_list_with_token_id + } + end + end) + + ordered_changes_list_no_token_id = + changes_list_no_token_id + |> Enum.group_by(fn %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number + } -> + {token_contract_address_hash, address_hash, block_number} + end) + |> Enum.map(fn {_, grouped_address_token_balances} -> + dedup = Enum.dedup(grouped_address_token_balances) + + if Enum.count(dedup) > 1 do + Enum.max_by(dedup, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end) + else + Enum.at(dedup, 0) + end + end) + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash, &1.block_number}) + + ordered_changes_list_with_token_id = + changes_list_with_token_id + |> Enum.group_by(fn %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id, + block_number: block_number + } -> + {token_contract_address_hash, token_id, address_hash, block_number} + end) + |> Enum.map(fn {_, grouped_address_token_balances} -> + if Enum.count(grouped_address_token_balances) > 1 do + Enum.max_by(grouped_address_token_balances, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end) + else + Enum.at(grouped_address_token_balances, 0) + end + end) + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash, &1.block_number}) + + {:ok, inserted_changes_list_no_token_id} = + if Enum.count(ordered_changes_list_no_token_id) > 0 do + Import.insert_changes_list( + repo, + ordered_changes_list_no_token_id, + conflict_target: + {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, block_number) WHERE token_id IS NULL>}, + on_conflict: on_conflict, + for: TokenBalance, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + else + {:ok, []} + end + + {:ok, inserted_changes_list_with_token_id} = + if Enum.count(ordered_changes_list_with_token_id) > 0 do + Import.insert_changes_list( + repo, + ordered_changes_list_with_token_id, + conflict_target: + {:unsafe_fragment, + ~s<(address_hash, token_contract_address_hash, token_id, block_number) WHERE token_id IS NOT NULL>}, + on_conflict: on_conflict, + for: TokenBalance, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + else + {:ok, []} + end + + inserted_changes_list = inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id + + {:ok, inserted_changes_list} end defp default_on_conflict do diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index b8d67208ce..b74cedf13c 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -119,13 +119,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do query = from(ctb in Address.CurrentTokenBalance, where: ctb.block_number in ^consensus_block_numbers, - select: ctb.token_contract_address_hash, - distinct: ctb.token_contract_address_hash + select: {ctb.token_contract_address_hash, ctb.token_id}, + distinct: [ctb.token_contract_address_hash, ctb.token_id] ) - contract_address_hashes = repo.all(query) + contract_address_hashes_and_token_ids = repo.all(query) - Tokens.acquire_contract_address_tokens(repo, contract_address_hashes) + Tokens.acquire_contract_address_tokens(repo, contract_address_hashes_and_token_ids) end defp fork_transactions(%{ @@ -341,10 +341,11 @@ defmodule Explorer.Chain.Import.Runner.Blocks do ordered_query = from(tb in Address.TokenBalance, where: tb.block_number in ^consensus_block_numbers, - select: map(tb, [:address_hash, :token_contract_address_hash, :block_number]), + select: map(tb, [:address_hash, :token_contract_address_hash, :token_id, :block_number]), # Enforce TokenBalance ShareLocks order (see docs: sharelocks.md) order_by: [ tb.token_contract_address_hash, + tb.token_id, tb.address_hash, tb.block_number ], @@ -359,6 +360,9 @@ defmodule Explorer.Chain.Import.Runner.Blocks do ordered_address_token_balance.address_hash == tb.address_hash and ordered_address_token_balance.token_contract_address_hash == tb.token_contract_address_hash and + ((is_nil(ordered_address_token_balance.token_id) and is_nil(tb.token_id)) or + (ordered_address_token_balance.token_id == tb.token_id and + not is_nil(ordered_address_token_balance.token_id) and not is_nil(tb.token_id))) and ordered_address_token_balance.block_number == tb.block_number ) @@ -378,10 +382,11 @@ defmodule Explorer.Chain.Import.Runner.Blocks do ordered_query = from(ctb in Address.CurrentTokenBalance, where: ctb.block_number in ^consensus_block_numbers, - select: map(ctb, [:address_hash, :token_contract_address_hash]), + select: map(ctb, [:address_hash, :token_contract_address_hash, :token_id]), # Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md) order_by: [ ctb.token_contract_address_hash, + ctb.token_id, ctb.address_hash ], lock: "FOR UPDATE" @@ -393,6 +398,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do map(ctb, [ :address_hash, :token_contract_address_hash, + :token_id, # Used to determine if `address_hash` was a holder of `token_contract_address_hash` before # `address_current_token_balance` is deleted in `update_tokens_holder_count`. @@ -401,8 +407,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do inner_join: ordered_address_current_token_balance in subquery(ordered_query), on: ordered_address_current_token_balance.address_hash == ctb.address_hash and - ordered_address_current_token_balance.token_contract_address_hash == - ctb.token_contract_address_hash + ordered_address_current_token_balance.token_contract_address_hash == ctb.token_contract_address_hash and + ((is_nil(ordered_address_current_token_balance.token_id) and is_nil(ctb.token_id)) or + (ordered_address_current_token_balance.token_id == ctb.token_id and + not is_nil(ordered_address_current_token_balance.token_id) and not is_nil(ctb.token_id))) ) try do @@ -417,31 +425,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do defp derive_address_current_token_balances(_, [], _), do: {:ok, []} - defp derive_address_current_token_balances(repo, deleted_address_current_token_balances, %{timeout: timeout}) + defp derive_address_current_token_balances( + repo, + deleted_address_current_token_balances, + %{timeout: timeout} = options + ) when is_list(deleted_address_current_token_balances) do - initial_query = - from(tb in Address.TokenBalance, - select: %{ - address_hash: tb.address_hash, - token_contract_address_hash: tb.token_contract_address_hash, - block_number: max(tb.block_number) - }, - group_by: [tb.address_hash, tb.token_contract_address_hash] - ) - - final_query = - Enum.reduce(deleted_address_current_token_balances, initial_query, fn %{ - address_hash: address_hash, - token_contract_address_hash: - token_contract_address_hash - }, - acc_query -> - from(tb in acc_query, - or_where: - tb.address_hash == ^address_hash and - tb.token_contract_address_hash == ^token_contract_address_hash - ) - end) + final_query = derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances) new_current_token_balance_query = from(new_current_token_balance in subquery(final_query), @@ -449,42 +439,82 @@ defmodule Explorer.Chain.Import.Runner.Blocks do on: tb.address_hash == new_current_token_balance.address_hash and tb.token_contract_address_hash == new_current_token_balance.token_contract_address_hash and + ((is_nil(tb.token_id) and is_nil(new_current_token_balance.token_id)) or + (tb.token_id == new_current_token_balance.token_id and + not is_nil(tb.token_id) and not is_nil(new_current_token_balance.token_id))) and tb.block_number == new_current_token_balance.block_number, select: %{ address_hash: new_current_token_balance.address_hash, token_contract_address_hash: new_current_token_balance.token_contract_address_hash, + token_id: new_current_token_balance.token_id, block_number: new_current_token_balance.block_number, value: tb.value, inserted_at: over(min(tb.inserted_at), :w), updated_at: over(max(tb.updated_at), :w) }, windows: [ - w: [partition_by: [tb.address_hash, tb.token_contract_address_hash]] + w: [partition_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id]] ] ) - ordered_current_token_balance = + current_token_balance = new_current_token_balance_query |> repo.all() - # Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md) - |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash}) - {_total, result} = - repo.insert_all( - Address.CurrentTokenBalance, - ordered_current_token_balance, - # No `ON CONFLICT` because `delete_address_current_token_balances` - # should have removed any conflicts. - returning: [:address_hash, :token_contract_address_hash, :block_number, :value], - timeout: timeout + timestamps = Import.timestamps() + + result = + CurrentTokenBalances.insert_changes_list_with_and_without_token_id( + current_token_balance, + repo, + timestamps, + timeout, + options ) derived_address_current_token_balances = - Enum.map(result, &Map.take(&1, [:address_hash, :token_contract_address_hash, :block_number, :value])) + Enum.map(result, &Map.take(&1, [:address_hash, :token_contract_address_hash, :token_id, :block_number, :value])) {:ok, derived_address_current_token_balances} end + defp derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances) do + initial_query = + from(tb in Address.TokenBalance, + select: %{ + address_hash: tb.address_hash, + token_contract_address_hash: tb.token_contract_address_hash, + token_id: tb.token_id, + block_number: max(tb.block_number) + }, + group_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id] + ) + + Enum.reduce(deleted_address_current_token_balances, initial_query, fn %{ + address_hash: address_hash, + token_contract_address_hash: + token_contract_address_hash, + token_id: token_id + }, + acc_query -> + if token_id do + from(tb in acc_query, + or_where: + tb.address_hash == ^address_hash and + tb.token_contract_address_hash == ^token_contract_address_hash and + tb.token_id == ^token_id + ) + else + from(tb in acc_query, + or_where: + tb.address_hash == ^address_hash and + tb.token_contract_address_hash == ^token_contract_address_hash and + is_nil(tb.token_id) + ) + end + end) + end + # `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is # inserted, the old block rewards need to be deleted, so that the old and new rewards aren't combined. defp delete_rewards(repo, blocks_changes, %{timeout: timeout}) do diff --git a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex index d6a254911c..1e24d15d29 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex @@ -21,17 +21,70 @@ defmodule Explorer.Chain.Import.Runner.Tokens do @type holder_count :: non_neg_integer() @type token_holder_count :: %{contract_address_hash: Hash.Address.t(), count: holder_count()} - def acquire_contract_address_tokens(repo, contract_address_hashes) do - token_query = - from( - token in Token, - where: token.contract_address_hash in ^contract_address_hashes, - # Enforce Token ShareLocks order (see docs: sharelocks.md) - order_by: token.contract_address_hash, - lock: "FOR UPDATE" + def acquire_contract_address_tokens(repo, contract_address_hashes_and_token_ids) do + initial_query_no_token_id = + from(token in Token, + select: token ) - tokens = repo.all(token_query) + initial_query_with_token_id = + from(token in Token, + left_join: instance in Token.Instance, + on: token.contract_address_hash == instance.token_contract_address_hash, + select: token + ) + + {query_no_token_id, query_with_token_id} = + contract_address_hashes_and_token_ids + |> Enum.reduce({initial_query_no_token_id, initial_query_with_token_id}, fn {contract_address_hash, token_id}, + {query_no_token_id, + query_with_token_id} -> + if is_nil(token_id) do + {from( + token in query_no_token_id, + or_where: token.contract_address_hash == ^contract_address_hash + ), query_with_token_id} + else + {query_no_token_id, + from( + [token, instance] in query_with_token_id, + or_where: token.contract_address_hash == ^contract_address_hash and instance.token_id == ^token_id + )} + end + end) + + final_query_no_token_id = + if query_no_token_id == initial_query_no_token_id do + nil + else + from( + token in query_no_token_id, + # Enforce Token ShareLocks order (see docs: sharelocks.md) + order_by: [ + token.contract_address_hash + ], + lock: "FOR UPDATE" + ) + end + + final_query_with_token_id = + if query_with_token_id == initial_query_with_token_id do + nil + else + from( + [token, instance] in query_with_token_id, + # Enforce Token ShareLocks order (see docs: sharelocks.md) + order_by: [ + token.contract_address_hash, + instance.token_id + ], + lock: "FOR UPDATE" + ) + end + + tokens_no_token_id = (final_query_no_token_id && repo.all(final_query_no_token_id)) || [] + tokens_with_token_id = (final_query_with_token_id && repo.all(final_query_with_token_id)) || [] + tokens = tokens_no_token_id ++ tokens_with_token_id {:ok, tokens} end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 990ac1fa34..686ee7c70f 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -8,6 +8,7 @@ defmodule Explorer.Chain.Token do * ERC-20 * ERC-721 + * ERC-1155 ## Token Specifications diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 29a93c5074..34ab5076c0 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -47,9 +47,11 @@ defmodule Explorer.Chain.TokenTransfer do * `:transaction` - The `t:Explorer.Chain.Transaction.t/0` ledger * `:transaction_hash` - Transaction foreign key * `:log_index` - Index of the corresponding `t:Explorer.Chain.Log.t/0` in the transaction. + * `:amounts` - Tokens transferred amounts in case of batched transfer in ERC-1155 + * `:token_ids` - IDs of the tokens (applicable to ERC-1155 tokens) """ @type t :: %TokenTransfer{ - amount: Decimal.t(), + amount: Decimal.t() | nil, block_number: non_neg_integer() | nil, block_hash: Hash.Full.t(), from_address: %Ecto.Association.NotLoaded{} | Address.t(), @@ -61,12 +63,16 @@ defmodule Explorer.Chain.TokenTransfer do token_id: non_neg_integer() | nil, transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), transaction_hash: Hash.Full.t(), - log_index: non_neg_integer() + log_index: non_neg_integer(), + amounts: [Decimal.t()] | nil, + token_ids: [non_neg_integer()] | nil } @typep paging_options :: {:paging_options, PagingOptions.t()} @constant "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @erc1155_single_transfer_signature "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" + @erc1155_batch_transfer_signature "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb" @transfer_function_signature "0xa9059cbb" @@ -76,6 +82,8 @@ defmodule Explorer.Chain.TokenTransfer do field(:block_number, :integer) field(:log_index, :integer, primary_key: true) field(:token_id, :decimal) + field(:amounts, {:array, :decimal}) + field(:token_ids, {:array, :decimal}) belongs_to(:from_address, Address, foreign_key: :from_address_hash, references: :hash, type: Hash.Address) belongs_to(:to_address, Address, foreign_key: :to_address_hash, references: :hash, type: Hash.Address) @@ -115,7 +123,7 @@ defmodule Explorer.Chain.TokenTransfer do end @required_attrs ~w(block_number log_index from_address_hash to_address_hash token_contract_address_hash transaction_hash block_hash)a - @optional_attrs ~w(amount token_id)a + @optional_attrs ~w(amount token_id amounts token_ids)a @doc false def changeset(%TokenTransfer{} = struct, params \\ %{}) do @@ -134,6 +142,10 @@ defmodule Explorer.Chain.TokenTransfer do """ def constant, do: @constant + def erc1155_single_transfer_signature, do: @erc1155_single_transfer_signature + + def erc1155_batch_transfer_signature, do: @erc1155_batch_transfer_signature + @doc """ ERC 20's transfer(address,uint256) function signature """ @@ -294,6 +306,7 @@ defmodule Explorer.Chain.TokenTransfer do left_join: instance in Instance, on: tt.token_contract_address_hash == instance.token_contract_address_hash and tt.token_id == instance.token_id, where: tt.token_contract_address_hash == ^contract_address_hash, + where: tt.to_address_hash != ^"0x0000000000000000000000000000000000000000", order_by: [desc: tt.block_number], distinct: [desc: tt.token_id], preload: [:to_address], diff --git a/apps/explorer/lib/explorer/token/balance_reader.ex b/apps/explorer/lib/explorer/token/balance_reader.ex index 22a45fec54..0a093c8cd7 100644 --- a/apps/explorer/lib/explorer/token/balance_reader.ex +++ b/apps/explorer/lib/explorer/token/balance_reader.ex @@ -27,6 +27,18 @@ defmodule Explorer.Token.BalanceReader do } ] + @erc1155_balance_function_abi [ + %{ + "constant" => true, + "inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}], + "name" => "balanceOf", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + @spec get_balances_of([ %{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()} ]) :: [{:ok, non_neg_integer()} | {:error, String.t()}] @@ -37,6 +49,31 @@ defmodule Explorer.Token.BalanceReader do |> Enum.map(&format_balance_result/1) end + @spec get_balances_of_with_abi( + [ + %{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()} + ], + [%{}] + ) :: [{:ok, non_neg_integer()} | {:error, String.t()}] + def get_balances_of_with_abi(token_balance_requests, abi) do + formatted_balances_requests = + if abi == @erc1155_balance_function_abi do + token_balance_requests + |> Enum.map(&format_erc_1155_balance_request/1) + else + token_balance_requests + |> Enum.map(&format_balance_request/1) + end + + if Enum.count(formatted_balances_requests) > 0 do + formatted_balances_requests + |> Reader.query_contracts(abi) + |> Enum.map(&format_balance_result/1) + else + [] + end + end + defp format_balance_request(%{ address_hash: address_hash, block_number: block_number, @@ -50,6 +87,20 @@ defmodule Explorer.Token.BalanceReader do } end + defp format_erc_1155_balance_request(%{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id + }) do + %{ + contract_address: token_contract_address_hash, + method_id: "00fdd58e", + args: [address_hash, token_id], + block_number: block_number + } + end + defp format_balance_result({:ok, [balance]}) do {:ok, balance} end diff --git a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex index e9c6c06ad3..7b87543e6f 100644 --- a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex @@ -29,9 +29,36 @@ defmodule Explorer.Token.InstanceMetadataRetriever do } ] + @uri "0e89341c" + + @abi_uri [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [ + %{ + "type" => "string", + "name" => "", + "internalType" => "string" + } + ], + "name" => "uri", + "inputs" => [ + %{ + "type" => "uint256", + "name" => "_id", + "internalType" => "uint256" + } + ], + "constant" => true + } + ] + @cryptokitties_address_hash "0x06012c8cf97bead5deae237070f9587f8e7a266d" @no_uri_error "no uri" + @vm_execution_error "VM execution error" def fetch_metadata(unquote(@cryptokitties_address_hash), token_id) do %{"tokenURI" => {:ok, ["https://api.cryptokitties.co/kitties/#{token_id}"]}} @@ -42,31 +69,62 @@ defmodule Explorer.Token.InstanceMetadataRetriever do # c87b56dd = keccak256(tokenURI(uint256)) contract_functions = %{@token_uri => [token_id]} - contract_address_hash - |> query_contract(contract_functions) - |> fetch_json() + res = + contract_address_hash + |> query_contract(contract_functions, @abi) + |> fetch_json() + + if res == {:ok, %{error: @vm_execution_error}} do + contract_functions_uri = %{@uri => [token_id]} + + contract_address_hash + |> query_contract(contract_functions_uri, @abi_uri) + |> fetch_json() + else + res + end end - def query_contract(contract_address_hash, contract_functions) do - Reader.query_contract(contract_address_hash, @abi, contract_functions) + def query_contract(contract_address_hash, contract_functions, abi) do + Reader.query_contract(contract_address_hash, abi, contract_functions) end - def fetch_json(%{@token_uri => {:ok, [""]}}) do + def fetch_json(uri) when uri in [%{@token_uri => {:ok, [""]}}, %{@uri => {:ok, [""]}}] do {:ok, %{error: @no_uri_error}} end - def fetch_json(%{@token_uri => {:error, "(-32015) VM execution error."}}) do - {:ok, %{error: @no_uri_error}} + def fetch_json(uri) + when uri in [ + %{@token_uri => {:error, "(-32015) VM execution error."}}, + %{@uri => {:error, "(-32015) VM execution error."}} + ] do + {:ok, %{error: @vm_execution_error}} + end + + def fetch_json(%{@token_uri => {:error, "(-32015) VM execution error." <> _}}) do + {:ok, %{error: @vm_execution_error}} + end + + def fetch_json(%{@uri => {:error, "(-32015) VM execution error." <> _}}) do + {:ok, %{error: @vm_execution_error}} end def fetch_json(%{@token_uri => {:ok, ["http://" <> _ = token_uri]}}) do fetch_metadata(token_uri) end + def fetch_json(%{@uri => {:ok, ["http://" <> _ = token_uri]}}) do + fetch_metadata(token_uri) + end + def fetch_json(%{@token_uri => {:ok, ["https://" <> _ = token_uri]}}) do fetch_metadata(token_uri) end + def fetch_json(%{@uri => {:ok, ["https://" <> _ = token_uri]}}) do + fetch_metadata(token_uri) + end + def fetch_json(%{@token_uri => {:ok, ["data:application/json," <> json]}}) do decoded_json = URI.decode(json) @@ -80,16 +138,39 @@ defmodule Explorer.Token.InstanceMetadataRetriever do {:error, json} end + def fetch_json(%{@uri => {:ok, ["data:application/json," <> json]}}) do + decoded_json = URI.decode(json) + + fetch_json(%{@token_uri => {:ok, [decoded_json]}}) + rescue + e -> + Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"], + fetcher: :token_instances + ) + + {:error, json} + end + def fetch_json(%{@token_uri => {:ok, ["ipfs://ipfs/" <> ipfs_uid]}}) do ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid fetch_metadata(ipfs_url) end + def fetch_json(%{@uri => {:ok, ["ipfs://ipfs/" <> ipfs_uid]}}) do + ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid + fetch_metadata(ipfs_url) + end + def fetch_json(%{@token_uri => {:ok, ["ipfs://" <> ipfs_uid]}}) do ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid fetch_metadata(ipfs_url) end + def fetch_json(%{@uri => {:ok, ["ipfs://" <> ipfs_uid]}}) do + ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid + fetch_metadata(ipfs_url) + end + def fetch_json(%{@token_uri => {:ok, [json]}}) do {:ok, json} = decode_json(json) @@ -103,17 +184,30 @@ defmodule Explorer.Token.InstanceMetadataRetriever do {:error, json} end + def fetch_json(%{@uri => {:ok, [json]}}) do + {:ok, json} = decode_json(json) + + check_type(json) + rescue + e -> + Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"], + fetcher: :token_instances + ) + + {:error, json} + end + def fetch_json(result) do Logger.debug(["Unknown metadata format #{inspect(result)}."], fetcher: :token_instances) {:error, result} end - defp fetch_metadata(token_uri) do - case HTTPoison.get(token_uri) do + defp fetch_metadata(uri) do + case HTTPoison.get(uri) do {:ok, %Response{body: body, status_code: 200, headers: headers}} -> if Enum.member?(headers, {"Content-Type", "image/png"}) do - json = %{"image" => token_uri} + json = %{"image" => uri} check_type(json) else @@ -135,7 +229,7 @@ defmodule Explorer.Token.InstanceMetadataRetriever do end rescue e -> - Logger.debug(["Could not send request to token uri #{inspect(token_uri)}. error #{inspect(e)}"], + Logger.debug(["Could not send request to token uri #{inspect(uri)}. error #{inspect(e)}"], fetcher: :token_instances ) diff --git a/apps/explorer/priv/repo/migrations/20200214152058_add_token_id_to_token_balances.exs b/apps/explorer/priv/repo/migrations/20200214152058_add_token_id_to_token_balances.exs new file mode 100644 index 0000000000..b009091f81 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20200214152058_add_token_id_to_token_balances.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.AddTokenIdToTokenBalances do + use Ecto.Migration + + def change do + alter table(:address_token_balances) do + add(:token_id, :numeric, precision: 78, scale: 0, null: true) + add(:token_type, :string, null: true) + end + + create(index(:address_token_balances, [:token_id])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20210422115740_add_token_id_to_current_token_balances.exs b/apps/explorer/priv/repo/migrations/20210422115740_add_token_id_to_current_token_balances.exs new file mode 100644 index 0000000000..4d8d870529 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20210422115740_add_token_id_to_current_token_balances.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.AddTokenIdToCurrentTokenBalances do + use Ecto.Migration + + def change do + alter table(:address_current_token_balances) do + add(:token_id, :numeric, precision: 78, scale: 0, null: true) + add(:token_type, :string, null: true) + end + + create(index(:address_current_token_balances, [:token_id])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20210423084253_address_current_token_balances_add_token_id_to_unique_index.exs b/apps/explorer/priv/repo/migrations/20210423084253_address_current_token_balances_add_token_id_to_unique_index.exs new file mode 100644 index 0000000000..b71de0448e --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20210423084253_address_current_token_balances_add_token_id_to_unique_index.exs @@ -0,0 +1,25 @@ +defmodule Explorer.Repo.Migrations.AddressCurrentTokenBalancesAddTokenIdToUniqueIndex do + use Ecto.Migration + + def change do + drop(unique_index(:address_current_token_balances, ~w(address_hash token_contract_address_hash)a)) + + create( + unique_index( + :address_current_token_balances, + ~w(address_hash token_contract_address_hash token_id)a, + name: :fetched_current_token_balances_with_token_id, + where: "token_id IS NOT NULL" + ) + ) + + create( + unique_index( + :address_current_token_balances, + ~w(address_hash token_contract_address_hash)a, + name: :fetched_current_token_balances, + where: "token_id IS NULL" + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20210423091652_address_token_balances_add_token_id_to_unique_index.exs b/apps/explorer/priv/repo/migrations/20210423091652_address_token_balances_add_token_id_to_unique_index.exs new file mode 100644 index 0000000000..1968f52132 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20210423091652_address_token_balances_add_token_id_to_unique_index.exs @@ -0,0 +1,25 @@ +defmodule Explorer.Repo.Migrations.AddressTokenBalancesAddTokenIdToUniqueIndex do + use Ecto.Migration + + def change do + drop(unique_index(:address_token_balances, ~w(address_hash token_contract_address_hash block_number)a)) + + create( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash token_id block_number)a, + name: :fetched_token_balances_with_token_id, + where: "token_id IS NOT NULL" + ) + ) + + create( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash block_number)a, + name: :fetched_token_balances, + where: "token_id IS NULL" + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20210423094801_address_token_balances_change_unfetched_token_balances_unique_index.exs b/apps/explorer/priv/repo/migrations/20210423094801_address_token_balances_change_unfetched_token_balances_unique_index.exs new file mode 100644 index 0000000000..911a7affbb --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20210423094801_address_token_balances_change_unfetched_token_balances_unique_index.exs @@ -0,0 +1,32 @@ +defmodule Explorer.Repo.Migrations.AddressTokenBalancesChangeUnfetchedTokenBalancesUniqueIndex do + use Ecto.Migration + + def change do + drop( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash block_number)a, + name: :unfetched_token_balances, + where: "value_fetched_at IS NULL" + ) + ) + + create( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash block_number)a, + name: :unfetched_token_balances, + where: "value_fetched_at IS NULL and token_id IS NULL" + ) + ) + + create( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash token_id block_number)a, + name: :unfetched_token_balances_with_token_id, + where: "value_fetched_at IS NULL and token_id IS NOT NULL" + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20210423115108_extend_token_transfers_for_erc1155.exs b/apps/explorer/priv/repo/migrations/20210423115108_extend_token_transfers_for_erc1155.exs new file mode 100644 index 0000000000..211524e954 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20210423115108_extend_token_transfers_for_erc1155.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Migrations.ExtendTokenTransfersForErc1155 do + use Ecto.Migration + + def change do + alter table(:token_transfers) do + add(:amounts, {:array, :decimal}, null: true) + add(:token_ids, {:array, :numeric}, precision: 78, scale: 0, null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20211013190346_remove_duplicates_of_current_token_balances.exs b/apps/explorer/priv/repo/migrations/20211013190346_remove_duplicates_of_current_token_balances.exs new file mode 100644 index 0000000000..ec26db4b73 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20211013190346_remove_duplicates_of_current_token_balances.exs @@ -0,0 +1,55 @@ +defmodule Explorer.Repo.Migrations.RemoveDuplicatesOfCurrentTokenBalances do + use Ecto.Migration + + def change do + execute(""" + DELETE FROM address_current_token_balances + WHERE id in ( + SELECT a.id FROM (SELECT actb.* + FROM address_current_token_balances actb + INNER JOIN tokens t + ON actb.token_contract_address_hash = t.contract_address_hash + WHERE t.type='ERC-721') AS a + LEFT JOIN + (SELECT actb.address_hash, actb.token_contract_address_hash, MAX(actb.value_fetched_at) AS max_value_fetched_at + FROM address_current_token_balances actb + INNER JOIN tokens t + ON actb.token_contract_address_hash = t.contract_address_hash + WHERE t.type='ERC-721' + GROUP BY token_contract_address_hash, address_hash) c + ON a.address_hash=c.address_hash AND a.token_contract_address_hash = c.token_contract_address_hash AND a.value_fetched_at = c.max_value_fetched_at + WHERE c.address_hash IS NULL + ); + """) + + execute(""" + UPDATE address_current_token_balances + SET token_id = NULL + WHERE id in ( + SELECT a.id FROM (SELECT actb.* + FROM address_current_token_balances actb + INNER JOIN tokens t + ON actb.token_contract_address_hash = t.contract_address_hash + WHERE t.type='ERC-721' + AND actb.token_id IS NOT NULL + ) a + ); + """) + + execute(""" + UPDATE address_current_token_balances + SET token_type = t.type + FROM tokens t + WHERE address_current_token_balances.token_type IS NULL + AND t.contract_address_hash = address_current_token_balances.token_contract_address_hash; + """) + + execute(""" + UPDATE address_token_balances + SET token_type = t.type + FROM tokens t + WHERE address_token_balances.token_type IS NULL + AND t.contract_address_hash = address_token_balances.token_contract_address_hash; + """) + end +end diff --git a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs index e7dcfe4b06..7c34c72719 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs @@ -65,6 +65,141 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do assert current_token_balances == 1 end + test "inserts values for multiple token IDs in the current token balances", %{ + address: %Address{hash: address_hash}, + token: %Token{contract_address_hash: token_contract_address_hash}, + options: options + } do + value_1 = Decimal.new(111) + token_id_1 = Decimal.new(1) + + value_2 = Decimal.new(222) + token_id_2 = Decimal.new(2) + + token_erc_20 = insert(:token, holder_count: 0) + token_erc_20_contract_address_hash = token_erc_20.contract_address_hash + value_3 = Decimal.new(333) + token_id_3 = nil + + token_erc_721 = insert(:token, holder_count: 0) + token_erc_721_contract_address_hash = token_erc_721.contract_address_hash + value_4 = Decimal.new(1) + token_id_4 = Decimal.new(1) + + value_5 = Decimal.new(2) + token_id_5 = Decimal.new(555) + + block_number = 1 + + assert {:ok, + %{ + address_current_token_balances: [ + %Explorer.Chain.Address.CurrentTokenBalance{ + address_hash: ^address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_erc_20_contract_address_hash, + value: ^value_3, + token_id: ^token_id_3 + }, + %Explorer.Chain.Address.CurrentTokenBalance{ + address_hash: ^address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_erc_721_contract_address_hash, + value: ^value_5, + token_id: nil + }, + %Explorer.Chain.Address.CurrentTokenBalance{ + address_hash: ^address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_contract_address_hash, + value: ^value_1, + token_id: ^token_id_1 + }, + %Explorer.Chain.Address.CurrentTokenBalance{ + address_hash: ^address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_contract_address_hash, + value: ^value_2, + token_id: ^token_id_2 + } + ], + address_current_token_balances_update_token_holder_counts: [ + %{ + contract_address_hash: ^token_contract_address_hash, + holder_count: 2 + }, + %{ + contract_address_hash: ^token_erc_20_contract_address_hash, + holder_count: 1 + }, + %{ + contract_address_hash: ^token_erc_721_contract_address_hash, + holder_count: 1 + } + ] + }} = + run_changes_list( + [ + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + value: value_1, + value_fetched_at: DateTime.utc_now(), + token_id: token_id_1, + token_type: "ERC-1155" + }, + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + value: value_2, + value_fetched_at: DateTime.utc_now(), + token_id: token_id_2, + token_type: "ERC-1155" + }, + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_erc_20.contract_address_hash, + value: value_3, + value_fetched_at: DateTime.utc_now(), + token_id: token_id_3, + token_type: "ERC-20" + }, + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_erc_721.contract_address_hash, + value: value_4, + value_fetched_at: DateTime.utc_now(), + token_id: token_id_4, + token_type: "ERC-721" + }, + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_erc_721.contract_address_hash, + value: value_5, + value_fetched_at: DateTime.utc_now(), + token_id: token_id_5, + token_type: "ERC-721" + } + ], + options + ) + + current_token_balances = + CurrentTokenBalance + |> Repo.all() + + current_token_balances_count = + current_token_balances + |> Enum.count() + + assert current_token_balances_count == 4 + end + test "updates when the new block number is greater", %{ address: address, token: token, diff --git a/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs index a35fbcc578..39e40c7e73 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs @@ -29,7 +29,9 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do block_number: block_number, token_contract_address_hash: token_contract_address_hash, value: value, - value_fetched_at: value_fetched_at + value_fetched_at: value_fetched_at, + token_id: 11, + token_type: "ERC-20" } assert {:ok, @@ -69,7 +71,9 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do block_number: block_number, token_contract_address_hash: token_contract_address_hash, value: nil, - value_fetched_at: value_fetched_at + value_fetched_at: value_fetched_at, + token_id: nil, + token_type: "ERC-20" } assert {:ok, @@ -97,6 +101,58 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do end end + test "does not nillifies existing value ERC-1155" do + address = insert(:address) + token = insert(:token) + + options = %{ + timeout: :infinity, + timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + } + + value_fetched_at = DateTime.utc_now() + + block_number = 1 + + value = Decimal.new(100) + + token_contract_address_hash = token.contract_address_hash + address_hash = address.hash + + changes = %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + value: nil, + value_fetched_at: value_fetched_at, + token_id: 11, + token_type: "ERC-1155" + } + + assert {:ok, + %{ + address_token_balances: [ + %TokenBalance{ + address_hash: address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_contract_address_hash, + value: nil, + value_fetched_at: ^value_fetched_at + } + ] + }} = run_changes(changes, options) + + new_changes = %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + value: value, + value_fetched_at: DateTime.utc_now() + } + + run_changes(new_changes, options) + end + defp run_changes(changes, options) when is_map(changes) do run_changes_list([changes], options) end diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index f96a536526..9cde0dbb81 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -375,17 +375,20 @@ defmodule Explorer.Chain.ImportTest do %{ address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", - block_number: "37" + block_number: "37", + token_type: "ERC-20" }, %{ address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", - block_number: "37" + block_number: "37", + token_type: "ERC-20" }, %{ address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", - block_number: "37" + block_number: "37", + token_type: "ERC-20" } ], timeout: 5 @@ -425,13 +428,15 @@ defmodule Explorer.Chain.ImportTest do address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", block_number: "37", - value: 200 + value: 200, + token_type: "ERC-20" }, %{ address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", block_number: "37", - value: 100 + value: 100, + token_type: "ERC-20" } ], timeout: 5 @@ -1564,8 +1569,8 @@ defmodule Explorer.Chain.ImportTest do }, address_coin_balances: %{ params: [ - %{address_hash: miner_hash, block_number: block_number, value: nil}, - %{address_hash: uncle_miner_hash, block_number: block_number, value: nil} + %{address_hash: miner_hash, block_number: block_number, value: nil, token_type: "ERC-20"}, + %{address_hash: uncle_miner_hash, block_number: block_number, value: nil, token_type: "ERC-20"} ], timeout: 1 }, @@ -2250,7 +2255,8 @@ defmodule Explorer.Chain.ImportTest do address_hash: address_hash, token_contract_address_hash: token_contract_address_hash, block_number: block_number, - value: value_after + value: value_after, + token_type: "ERC-20" } ] }, @@ -2260,7 +2266,8 @@ defmodule Explorer.Chain.ImportTest do address_hash: address_hash, token_contract_address_hash: token_contract_address_hash, block_number: block_number, - value: value_after + value: value_after, + token_type: "ERC-20" } ] }, diff --git a/apps/explorer/test/explorer/token/balance_reader_test.exs b/apps/explorer/test/explorer/token/balance_reader_test.exs index 1a98dd2455..126e903cde 100644 --- a/apps/explorer/test/explorer/token/balance_reader_test.exs +++ b/apps/explorer/test/explorer/token/balance_reader_test.exs @@ -32,7 +32,8 @@ defmodule Explorer.Token.BalanceReaderTest do %{ token_contract_address_hash: token_contract_address_hash, address_hash: address_hash, - block_number: block_number + block_number: block_number, + token_type: "ERC-20" } ]) @@ -51,7 +52,8 @@ defmodule Explorer.Token.BalanceReaderTest do %{ token_contract_address_hash: token_contract_address_hash, address_hash: address_hash, - block_number: block_number + block_number: block_number, + token_type: "ERC-20" } ]) diff --git a/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs b/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs index eec3b38e49..3afe5c937f 100644 --- a/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs @@ -9,6 +9,49 @@ defmodule Explorer.Token.InstanceMetadataRetrieverTest do setup :verify_on_exit! setup :set_mox_global + @abi [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [ + %{"type" => "string", "name" => ""} + ], + "name" => "tokenURI", + "inputs" => [ + %{ + "type" => "uint256", + "name" => "_tokenId" + } + ], + "constant" => true + } + ] + + @abi_uri [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [ + %{ + "type" => "string", + "name" => "", + "internalType" => "string" + } + ], + "name" => "uri", + "inputs" => [ + %{ + "type" => "uint256", + "name" => "_id", + "internalType" => "uint256" + } + ], + "constant" => true + } + ] + describe "fetch_metadata/2" do @tag :no_parity @tag :no_geth @@ -46,9 +89,56 @@ defmodule Explorer.Token.InstanceMetadataRetrieverTest do assert %{ "c87b56dd" => {:ok, ["https://vault.warriders.com/18290729947667102496.json"]} } == - InstanceMetadataRetriever.query_contract("0x5caebd3b32e210e85ce3e9d51638b9c445481567", %{ - "c87b56dd" => [18_290_729_947_667_102_496] - }) + InstanceMetadataRetriever.query_contract( + "0x5caebd3b32e210e85ce3e9d51638b9c445481567", + %{ + "c87b56dd" => [18_290_729_947_667_102_496] + }, + @abi + ) + end + + test "fetches json metadata for ERC-1155 token", %{json_rpc_named_arguments: json_rpc_named_arguments} do + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: + "0x0e89341c000000000000000000000000000000000000000000000000fdd5b9fa9d4bfb20", + to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" + }, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003568747470733a2f2f7661756c742e7761727269646572732e636f6d2f31383239303732393934373636373130323439362e6a736f6e0000000000000000000000" + } + ]} + end) + end + + assert %{ + "0e89341c" => {:ok, ["https://vault.warriders.com/18290729947667102496.json"]} + } == + InstanceMetadataRetriever.query_contract( + "0x5caebd3b32e210e85ce3e9d51638b9c445481567", + %{ + "0e89341c" => [18_290_729_947_667_102_496] + }, + @abi_uri + ) end end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index b76b2438e4..63ed106510 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -586,7 +586,8 @@ defmodule Explorer.Factory do token_contract_address_hash: insert(:token).contract_address_hash, block_number: block_number(), value: Enum.random(1..100_000), - value_fetched_at: DateTime.utc_now() + value_fetched_at: DateTime.utc_now(), + token_type: "ERC-20" } end diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex index fe32ac1943..1f34d3108c 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex @@ -96,7 +96,7 @@ defmodule Indexer.Fetcher.TokenBalance do retryable_params_list = params_list |> Enum.filter(&(&1.retries_count <= @max_retries)) - |> Enum.uniq_by(&Map.take(&1, [:token_contract_address_hash, :address_hash, :block_number])) + |> Enum.uniq_by(&Map.take(&1, [:token_contract_address_hash, :token_id, :address_hash, :block_number])) Logger.metadata(count: Enum.count(retryable_params_list)) @@ -138,15 +138,26 @@ defmodule Indexer.Fetcher.TokenBalance do %{ token_contract_address_hash: token_contract_address_hash, address_hash: address_hash, - block_number: block_number + block_number: block_number, + token_type: token_type, + token_id: token_id } = token_balance ) do retries_count = Map.get(token_balance, :retries_count, 0) - {address_hash.bytes, token_contract_address_hash.bytes, block_number, retries_count} + token_id_int = + case token_id do + %Decimal{} -> Decimal.to_integer(token_id) + id_int when is_integer(id_int) -> id_int + _ -> token_id + end + + {address_hash.bytes, token_contract_address_hash.bytes, block_number, token_type, token_id_int, retries_count} end - defp format_params({address_hash_bytes, token_contract_address_hash_bytes, block_number, retries_count}) do + defp format_params( + {address_hash_bytes, token_contract_address_hash_bytes, block_number, token_type, token_id, retries_count} + ) do {:ok, token_contract_address_hash} = Hash.Address.cast(token_contract_address_hash_bytes) {:ok, address_hash} = Hash.Address.cast(address_hash_bytes) @@ -154,7 +165,9 @@ defmodule Indexer.Fetcher.TokenBalance do token_contract_address_hash: to_string(token_contract_address_hash), address_hash: to_string(address_hash), block_number: block_number, - retries_count: retries_count + retries_count: retries_count, + token_type: token_type, + token_id: token_id } end end diff --git a/apps/indexer/lib/indexer/token_balances.ex b/apps/indexer/lib/indexer/token_balances.ex index 9a10dad13c..3e60f23260 100644 --- a/apps/indexer/lib/indexer/token_balances.ex +++ b/apps/indexer/lib/indexer/token_balances.ex @@ -13,6 +13,18 @@ defmodule Indexer.TokenBalances do alias Indexer.Fetcher.TokenBalance alias Indexer.Tracer + @erc1155_balance_function_abi [ + %{ + "constant" => true, + "inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}], + "name" => "balanceOf", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + @doc """ Fetches TokenBalances from specific Addresses and Blocks in the Blockchain @@ -26,6 +38,8 @@ defmodule Indexer.TokenBalances do * `token_contract_address_hash` - The contract address that represents the Token in the blockchain. * `address_hash` - The address_hash that we want to know the balance. * `block_number` - The block number that the address_hash has the balance. + * `token_type` - type of the token that balance belongs to + * `token_id` - token id for ERC-1155 tokens """ def fetch_token_balances_from_blockchain([]), do: {:ok, []} @@ -33,12 +47,39 @@ defmodule Indexer.TokenBalances do def fetch_token_balances_from_blockchain(token_balances) do Logger.debug("fetching token balances", count: Enum.count(token_balances)) - requested_token_balances = + regular_token_balances = + token_balances + |> Enum.filter(fn request -> + if Map.has_key?(request, :token_type) do + request.token_type !== "ERC-1155" + else + true + end + end) + + erc1155_token_balances = token_balances + |> Enum.filter(fn request -> + if Map.has_key?(request, :token_type) do + request.token_type == "ERC-1155" + else + false + end + end) + + requested_regular_token_balances = + regular_token_balances |> BalanceReader.get_balances_of() - |> Stream.zip(token_balances) + |> Stream.zip(regular_token_balances) |> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end) + requested_erc1155_token_balances = + erc1155_token_balances + |> BalanceReader.get_balances_of_with_abi(@erc1155_balance_function_abi) + |> Stream.zip(erc1155_token_balances) + |> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end) + + requested_token_balances = requested_regular_token_balances ++ requested_erc1155_token_balances fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1) requested_token_balances @@ -51,13 +92,17 @@ defmodule Indexer.TokenBalances do def to_address_current_token_balances(address_token_balances) when is_list(address_token_balances) do address_token_balances - |> Enum.group_by(fn %{address_hash: address_hash, token_contract_address_hash: token_contract_address_hash} -> - {address_hash, token_contract_address_hash} + |> Enum.group_by(fn %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id + } -> + {address_hash, token_contract_address_hash, token_id} end) |> Enum.map(fn {_, grouped_address_token_balances} -> Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end) end) - |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash}) + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash}) end defp set_token_balance_value({:ok, balance}, token_balance) do @@ -137,10 +182,20 @@ defmodule Indexer.TokenBalances do end defp present?(list, token_balance) do - Enum.any?(list, fn item -> - token_balance.address_hash == item.address_hash && - token_balance.token_contract_address_hash == item.token_contract_address_hash && - token_balance.block_number == item.block_number - end) + if token_balance.token_id do + Enum.any?(list, fn item -> + token_balance.address_hash == item.address_hash && + token_balance.token_contract_address_hash == item.token_contract_address_hash && + token_balance.token_id == item.token_id && + token_balance.block_number == item.block_number + end) + else + Enum.any?(list, fn item -> + token_balance.address_hash == item.address_hash && + token_balance.token_contract_address_hash == item.token_contract_address_hash && + is_nil(item.token_id) && + token_balance.block_number == item.block_number + end) + end end end diff --git a/apps/indexer/lib/indexer/transform/address_token_balances.ex b/apps/indexer/lib/indexer/transform/address_token_balances.ex index ff87ba7d57..ab6bb02fee 100644 --- a/apps/indexer/lib/indexer/transform/address_token_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_token_balances.ex @@ -16,14 +16,25 @@ defmodule Indexer.Transform.AddressTokenBalances do block_number: block_number, from_address_hash: from_address_hash, to_address_hash: to_address_hash, - token_contract_address_hash: token_contract_address_hash - }, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id, + token_type: token_type + } = params, acc when is_integer(block_number) and is_binary(from_address_hash) and is_binary(to_address_hash) and is_binary(token_contract_address_hash) -> - acc - |> add_token_balance_address(from_address_hash, token_contract_address_hash, block_number) - |> add_token_balance_address(to_address_hash, token_contract_address_hash, block_number) + if params[:token_ids] && token_type == "ERC-1155" do + params[:token_ids] + |> Enum.reduce(acc, fn id, sub_acc -> + sub_acc + |> add_token_balance_address(from_address_hash, token_contract_address_hash, id, token_type, block_number) + |> add_token_balance_address(to_address_hash, token_contract_address_hash, id, token_type, block_number) + end) + else + acc + |> add_token_balance_address(from_address_hash, token_contract_address_hash, token_id, token_type, block_number) + |> add_token_balance_address(to_address_hash, token_contract_address_hash, token_id, token_type, block_number) + end end) end @@ -31,13 +42,15 @@ defmodule Indexer.Transform.AddressTokenBalances do Enum.filter(token_transfers_params, &do_filter_burn_address/1) end - defp add_token_balance_address(map_set, unquote(@burn_address), _, _), do: map_set + defp add_token_balance_address(map_set, unquote(@burn_address), _, _, _, _), do: map_set - defp add_token_balance_address(map_set, address, token_contract_address, block_number) do + defp add_token_balance_address(map_set, address, token_contract_address, token_id, token_type, block_number) do MapSet.put(map_set, %{ address_hash: address, token_contract_address_hash: token_contract_address, - block_number: block_number + block_number: block_number, + token_id: token_id, + token_type: token_type }) end diff --git a/apps/indexer/lib/indexer/transform/token_transfers.ex b/apps/indexer/lib/indexer/transform/token_transfers.ex index ade12ef893..3d5ea4ed02 100644 --- a/apps/indexer/lib/indexer/transform/token_transfers.ex +++ b/apps/indexer/lib/indexer/transform/token_transfers.ex @@ -18,12 +18,21 @@ defmodule Indexer.Transform.TokenTransfers do def parse(logs) do initial_acc = %{tokens: [], token_transfers: []} - token_transfers_from_logs = + erc20_and_erc721_token_transfers = logs |> Enum.filter(&(&1.first_topic == unquote(TokenTransfer.constant()))) |> Enum.reduce(initial_acc, &do_parse/2) - token_transfers = token_transfers_from_logs.token_transfers + erc1155_token_transfers = + logs + |> Enum.filter(fn log -> + log.first_topic == TokenTransfer.erc1155_single_transfer_signature() || + log.first_topic == TokenTransfer.erc1155_batch_transfer_signature() + end) + |> Enum.reduce(initial_acc, &do_parse(&1, &2, :erc1155)) + + tokens = erc1155_token_transfers.tokens ++ erc20_and_erc721_token_transfers.tokens + token_transfers = erc1155_token_transfers.token_transfers ++ erc20_and_erc721_token_transfers.token_transfers token_transfers |> Enum.filter(fn token_transfer -> @@ -35,18 +44,23 @@ defmodule Indexer.Transform.TokenTransfers do |> Enum.dedup() |> Enum.each(&update_token/1) - tokens_dedup = token_transfers_from_logs.tokens |> Enum.dedup() + tokens_dedup = tokens |> Enum.dedup() token_transfers_from_logs_dedup = %{ tokens: tokens_dedup, - token_transfers: token_transfers_from_logs.token_transfers + token_transfers: token_transfers } token_transfers_from_logs_dedup end - defp do_parse(log, %{tokens: tokens, token_transfers: token_transfers} = acc) do - {token, token_transfer} = parse_params(log) + defp do_parse(log, %{tokens: tokens, token_transfers: token_transfers} = acc, type \\ :erc20_erc721) do + {token, token_transfer} = + if type != :erc1155 do + parse_params(log) + else + parse_erc1155_params(log) + end %{ tokens: [token | tokens], @@ -72,6 +86,7 @@ defmodule Indexer.Transform.TokenTransfers do to_address_hash: truncate_address_hash(log.third_topic), token_contract_address_hash: log.address_hash, transaction_hash: log.transaction_hash, + token_id: nil, token_type: "ERC-20" } @@ -109,7 +124,14 @@ defmodule Indexer.Transform.TokenTransfers do end # ERC-721 token transfer with info in data field instead of in log topics - defp parse_params(%{second_topic: nil, third_topic: nil, fourth_topic: nil, data: data} = log) + defp parse_params( + %{ + second_topic: nil, + third_topic: nil, + fourth_topic: nil, + data: data + } = log + ) when not is_nil(data) do [from_address_hash, to_address_hash, token_id] = decode_data(data, [:address, :address, {:uint, 256}]) @@ -157,6 +179,62 @@ defmodule Indexer.Transform.TokenTransfers do :ok end + def parse_erc1155_params( + %{ + first_topic: unquote(TokenTransfer.erc1155_batch_transfer_signature()), + third_topic: third_topic, + fourth_topic: fourth_topic, + data: data + } = log + ) do + [token_ids, values] = decode_data(data, [{:array, {:uint, 256}}, {:array, {:uint, 256}}]) + + token_transfer = %{ + block_number: log.block_number, + block_hash: log.block_hash, + log_index: log.index, + from_address_hash: truncate_address_hash(third_topic), + to_address_hash: truncate_address_hash(fourth_topic), + token_contract_address_hash: log.address_hash, + transaction_hash: log.transaction_hash, + token_type: "ERC-1155", + token_ids: token_ids, + token_id: nil, + amounts: values + } + + token = %{ + contract_address_hash: log.address_hash, + type: "ERC-1155" + } + + {token, token_transfer} + end + + def parse_erc1155_params(%{third_topic: third_topic, fourth_topic: fourth_topic, data: data} = log) do + [token_id, value] = decode_data(data, [{:uint, 256}, {:uint, 256}]) + + token_transfer = %{ + amount: value, + block_number: log.block_number, + block_hash: log.block_hash, + log_index: log.index, + from_address_hash: truncate_address_hash(third_topic), + to_address_hash: truncate_address_hash(fourth_topic), + token_contract_address_hash: log.address_hash, + transaction_hash: log.transaction_hash, + token_type: "ERC-1155", + token_id: token_id + } + + token = %{ + contract_address_hash: log.address_hash, + type: "ERC-1155" + } + + {token, token_transfer} + end + defp truncate_address_hash(nil), do: "0x0000000000000000000000000000000000000000" defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do diff --git a/apps/indexer/test/indexer/fetcher/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/token_balance_test.exs index 2ecee3eacd..f5879aac6b 100644 --- a/apps/indexer/test/indexer/fetcher/token_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_balance_test.exs @@ -17,13 +17,13 @@ defmodule Indexer.Fetcher.TokenBalanceTest do %Address.TokenBalance{ address_hash: %Hash{bytes: address_hash_bytes}, token_contract_address_hash: %Hash{bytes: token_contract_address_hash_bytes}, - block_number: block_number + block_number: _block_number } = insert(:token_balance, block_number: 1_000, value_fetched_at: nil) insert(:token_balance, value_fetched_at: DateTime.utc_now()) assert TokenBalance.init([], &[&1 | &2], nil) == [ - {address_hash_bytes, token_contract_address_hash_bytes, block_number, 0} + {address_hash_bytes, token_contract_address_hash_bytes, 1000, "ERC-20", nil, 0} ] end end @@ -58,7 +58,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do ) assert TokenBalance.run( - [{address_hash_bytes, token_contract_address_hash_bytes, block_number, 0}], + [{address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0}], nil ) == :ok @@ -76,26 +76,12 @@ defmodule Indexer.Fetcher.TokenBalanceTest do token_balance_a = insert(:token_balance, value_fetched_at: nil, value: nil) token_balance_b = insert(:token_balance, value_fetched_at: nil, value: nil) - expect( - EthereumJSONRPC.Mox, - :json_rpc, - 1, - fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, - id: id, - jsonrpc: "2.0" - } - ]} - end - ) - token_balances = [ { token_balance_a.address_hash.bytes, token_balance_a.token_contract_address_hash.bytes, + "ERC-20", + nil, token_balance_a.block_number, # this token balance must be ignored max_retries @@ -103,6 +89,8 @@ defmodule Indexer.Fetcher.TokenBalanceTest do { token_balance_b.address_hash.bytes, token_balance_b.token_contract_address_hash.bytes, + "ERC-20", + nil, token_balance_b.block_number, # this token balance still have to be retried max_retries - 2 @@ -136,8 +124,8 @@ defmodule Indexer.Fetcher.TokenBalanceTest do assert TokenBalance.run( [ - {address_hash_bytes, token_contract_address_hash_bytes, block_number, 0}, - {address_hash_bytes, token_contract_address_hash_bytes, block_number, 0} + {address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0}, + {address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0} ], nil ) == :ok @@ -161,6 +149,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do address_hash: nil, block_number: nil, token_contract_address_hash: to_string(token_balance.token_contract_address_hash), + token_id: nil, value: nil, value_fetched_at: nil } @@ -177,7 +166,9 @@ defmodule Indexer.Fetcher.TokenBalanceTest do %{ address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", block_number: 19999, - token_contract_address_hash: to_string(contract.contract_address_hash) + token_contract_address_hash: to_string(contract.contract_address_hash), + token_type: "ERC-20", + token_id: nil } ] @@ -186,7 +177,32 @@ defmodule Indexer.Fetcher.TokenBalanceTest do assert {:ok, _} = Explorer.Chain.hash_to_address(address_hash) end - test "import the token balances and return :ok when there are multiple balances for the same address on the batch" do + test "import the token balances and return :ok when there are multiple balances for the same address on the batch (ERC-20)" do + contract = insert(:token) + contract2 = insert(:token) + insert(:block, number: 19999) + + token_balances_params = [ + %{ + address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + block_number: 19999, + token_contract_address_hash: to_string(contract.contract_address_hash), + token_id: nil, + token_type: "ERC-20" + }, + %{ + address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + block_number: 19999, + token_contract_address_hash: to_string(contract2.contract_address_hash), + token_id: nil, + token_type: "ERC-20" + } + ] + + assert TokenBalance.import_token_balances(token_balances_params) == :ok + end + + test "import the token balances and return :ok when there are multiple balances for the same address on the batch (ERC-1155)" do contract = insert(:token) contract2 = insert(:token) insert(:block, number: 19999) @@ -195,12 +211,16 @@ defmodule Indexer.Fetcher.TokenBalanceTest do %{ address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", block_number: 19999, - token_contract_address_hash: to_string(contract.contract_address_hash) + token_contract_address_hash: to_string(contract.contract_address_hash), + token_id: 11, + token_type: "ERC-20" }, %{ address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", block_number: 19999, - token_contract_address_hash: to_string(contract2.contract_address_hash) + token_contract_address_hash: to_string(contract2.contract_address_hash), + token_id: 11, + token_type: "ERC-1155" } ] diff --git a/apps/indexer/test/indexer/token_balances_test.exs b/apps/indexer/test/indexer/token_balances_test.exs index 6c2eccf373..8752012387 100644 --- a/apps/indexer/test/indexer/token_balances_test.exs +++ b/apps/indexer/test/indexer/token_balances_test.exs @@ -26,10 +26,14 @@ defmodule Indexer.TokenBalancesTest do token = insert(:token, contract_address: build(:contract_address)) address_hash_string = Hash.to_string(address.hash) + token_contract_address_hash = Hash.to_string(token.contract_address_hash) + data = %{ - token_contract_address_hash: Hash.to_string(token.contract_address_hash), + token_contract_address_hash: token_contract_address_hash, address_hash: address_hash_string, - block_number: 1_000 + block_number: 1_000, + token_id: 11, + token_type: "ERC-20" } get_balance_from_blockchain() @@ -38,13 +42,183 @@ defmodule Indexer.TokenBalancesTest do assert %{ value: 1_000_000_000_000_000_000_000_000, - token_contract_address_hash: token_contract_address_hash, - address_hash: address_hash, + token_contract_address_hash: ^token_contract_address_hash, + address_hash: ^address_hash_string, block_number: 1_000, value_fetched_at: _ } = List.first(result) end + test "fetches balances of ERC-1155 tokens" do + address = insert(:address, hash: "0x609991ca0ae39bc4eaf2669976237296d40c2f31") + + address_hash_string = Hash.to_string(address.hash) + + token_contract_address_hash = "0xf7f79032fd395978acb7069c74d21e5a53206559" + + contract_address = insert(:address, hash: token_contract_address_hash) + + token = insert(:token, contract_address: contract_address) + + data = [ + %{ + token_contract_address_hash: Hash.to_string(token.contract_address_hash), + address_hash: address_hash_string, + block_number: 1_000, + token_id: 5, + token_type: "ERC-1155" + } + ] + + get_erc1155_balance_from_blockchain() + + {:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(data) + + assert [ + %{ + value: 2, + token_contract_address_hash: ^token_contract_address_hash, + address_hash: ^address_hash_string, + block_number: 1_000, + value_fetched_at: _ + } + ] = result + end + + test "fetches multiple balances of tokens" do + address_1 = insert(:address, hash: "0xecba3c9ea993b0e0594e0b0a0d361a1f9596e310") + address_2 = insert(:address, hash: "0x609991ca0ae39bc4eaf2669976237296d40c2f31") + address_3 = insert(:address, hash: "0xf712a82dd8e2ac923299193e9d6daeda2d5a32fd") + + address_1_hash_string = Hash.to_string(address_1.hash) + address_2_hash_string = Hash.to_string(address_2.hash) + address_3_hash_string = Hash.to_string(address_3.hash) + + token_1_contract_address_hash = "0x57e93bb58268de818b42e3795c97bad58afcd3fe" + token_2_contract_address_hash = "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da" + token_3_contract_address_hash = "0x22c1f6050e56d2876009903609a2cc3fef83b415" + token_4_contract_address_hash = "0xf7f79032fd395978acb7069c74d21e5a53206559" + + contract_address_1 = insert(:address, hash: token_1_contract_address_hash) + contract_address_2 = insert(:address, hash: token_2_contract_address_hash) + contract_address_3 = insert(:address, hash: token_3_contract_address_hash) + contract_address_4 = insert(:address, hash: token_4_contract_address_hash) + + token_1 = insert(:token, contract_address: contract_address_1) + token_2 = insert(:token, contract_address: contract_address_2) + token_3 = insert(:token, contract_address: contract_address_3) + token_4 = insert(:token, contract_address: contract_address_4) + + data = [ + %{ + token_contract_address_hash: Hash.to_string(token_1.contract_address_hash), + address_hash: address_1_hash_string, + block_number: 1_000, + token_id: nil, + token_type: "ERC-20" + }, + %{ + token_contract_address_hash: Hash.to_string(token_2.contract_address_hash), + address_hash: address_2_hash_string, + block_number: 1_000, + token_id: nil, + token_type: "ERC-20" + }, + %{ + token_contract_address_hash: Hash.to_string(token_3.contract_address_hash), + address_hash: address_2_hash_string, + block_number: 1_000, + token_id: 42, + token_type: "ERC-721" + }, + %{ + token_contract_address_hash: Hash.to_string(token_4.contract_address_hash), + address_hash: address_2_hash_string, + block_number: 1_000, + token_id: 5, + token_type: "ERC-1155" + }, + %{ + token_contract_address_hash: Hash.to_string(token_2.contract_address_hash), + address_hash: Hash.to_string(token_2.contract_address_hash), + block_number: 1_000, + token_id: nil, + token_type: "ERC-20" + }, + %{ + token_contract_address_hash: Hash.to_string(token_2.contract_address_hash), + address_hash: address_3_hash_string, + block_number: 1_000, + token_id: nil, + token_type: "ERC-20" + }, + %{ + token_contract_address_hash: Hash.to_string(token_2.contract_address_hash), + address_hash: Hash.to_string(token_2.contract_address_hash), + block_number: 1_000, + token_id: nil, + token_type: "ERC-20" + } + ] + + get_multiple_balances_from_blockchain() + get_erc1155_balance_from_blockchain() + + {:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(data) + + assert [ + %{ + value: 1_000_000_000_000_000_000_000_000, + token_contract_address_hash: ^token_1_contract_address_hash, + address_hash: ^address_1_hash_string, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + value: 3_000_000_000_000_000_000_000_000_000, + token_contract_address_hash: ^token_2_contract_address_hash, + address_hash: ^address_2_hash_string, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + value: 1, + token_contract_address_hash: ^token_3_contract_address_hash, + address_hash: ^address_2_hash_string, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + value: 6_000_000_000_000_000_000_000_000_000, + token_contract_address_hash: ^token_2_contract_address_hash, + address_hash: ^token_2_contract_address_hash, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + value: 5_000_000_000_000_000_000_000_000_000, + token_contract_address_hash: ^token_2_contract_address_hash, + address_hash: ^address_3_hash_string, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + value: 6_000_000_000_000_000_000_000_000_000, + token_contract_address_hash: ^token_2_contract_address_hash, + address_hash: ^token_2_contract_address_hash, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + value: 2, + token_contract_address_hash: ^token_4_contract_address_hash, + address_hash: ^address_2_hash_string, + block_number: 1_000, + value_fetched_at: _ + } + ] = result + end + test "ignores calls that gave errors to try fetch they again later" do address = insert(:address, hash: "0x7113ffcb9c18a97da1b9cfc43e6cb44ed9165509") token = insert(:token, contract_address: build(:contract_address)) @@ -54,7 +228,9 @@ defmodule Indexer.TokenBalancesTest do address_hash: to_string(address.hash), block_number: 1_000, token_contract_address_hash: to_string(token.contract_address_hash), - retries_count: 1 + retries_count: 1, + token_id: 11, + token_type: "ERC-20" } ] @@ -128,12 +304,14 @@ defmodule Indexer.TokenBalancesTest do token_balance_a = %{ token_contract_address_hash: Hash.to_string(token.contract_address_hash), + token_id: nil, address_hash: address_hash_string, block_number: 1_000 } token_balance_b = %{ token_contract_address_hash: Hash.to_string(token.contract_address_hash), + token_id: nil, address_hash: address_hash_string, block_number: 1_001 } @@ -162,6 +340,129 @@ defmodule Indexer.TokenBalancesTest do ) end + defp get_erc1155_balance_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn requests, _options -> + {:ok, + requests + |> Enum.map(fn + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0x00fdd58e000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f310000000000000000000000000000000000000000000000000000000000000005", + to: "0xf7f79032fd395978acb7069c74d21e5a53206559" + }, + _ + ] + } -> + %{ + id: id, + jsonrpc: "2.0", + result: "0x0000000000000000000000000000000000000000000000000000000000000002" + } + + req -> + IO.inspect("Gimme req") + IO.inspect(req) + end) + |> Enum.shuffle()} + end + ) + end + + defp get_multiple_balances_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn requests, _options -> + {:ok, + requests + |> Enum.map(fn + %{id: id, method: "eth_call", params: [%{data: _, to: "0x57e93bb58268de818b42e3795c97bad58afcd3fe"}, _]} -> + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + } + + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0x70a08231000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f31", + to: "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da" + }, + _ + ] + } -> + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000000000000000000009b18ab5df7180b6b8000000" + } + + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0x70a08231000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f31", + to: "0x22c1f6050e56d2876009903609a2cc3fef83b415" + }, + _ + ] + } -> + %{ + id: id, + jsonrpc: "2.0", + result: "0x0000000000000000000000000000000000000000000000000000000000000001" + } + + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0x70a08231000000000000000000000000f712a82dd8e2ac923299193e9d6daeda2d5a32fd", + to: "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da" + }, + _ + ] + } -> + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000001027e72f1f12813088000000" + } + + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0x70a08231000000000000000000000000e0d0b1dbbcf3dd5cac67edaf9243863fd70745da", + to: "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da" + }, + _ + ] + } -> + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000001363156bbee3016d70000000" + } + end) + |> Enum.shuffle()} + end + ) + end + defp get_balance_from_blockchain_with_error() do expect( EthereumJSONRPC.Mox, diff --git a/apps/indexer/test/indexer/transform/address_token_balances_test.exs b/apps/indexer/test/indexer/transform/address_token_balances_test.exs index 1e22696557..82009ffab2 100644 --- a/apps/indexer/test/indexer/transform/address_token_balances_test.exs +++ b/apps/indexer/test/indexer/transform/address_token_balances_test.exs @@ -24,7 +24,9 @@ defmodule Indexer.Transform.AddressTokenBalancesTest do block_number: block_number, from_address_hash: from_address_hash, to_address_hash: to_address_hash, - token_contract_address_hash: token_contract_address_hash + token_contract_address_hash: token_contract_address_hash, + token_id: nil, + token_type: "ERC-20" } params_set = AddressTokenBalances.params_set(%{token_transfers_params: [token_transfer_params]}) @@ -46,7 +48,8 @@ defmodule Indexer.Transform.AddressTokenBalancesTest do from_address_hash: from_address_hash, to_address_hash: to_address_hash, token_contract_address_hash: token_contract_address_hash, - token_type: "ERC-721" + token_type: "ERC-721", + token_id: nil } params_set = AddressTokenBalances.params_set(%{token_transfers_params: [token_transfer_params]}) @@ -56,7 +59,9 @@ defmodule Indexer.Transform.AddressTokenBalancesTest do %{ address_hash: "0x5b8410f67eb8040bb1cd1e8a4ff9d5f6ce678a15", block_number: 1, - token_contract_address_hash: "0xe18035bf8712672935fdb4e5e431b1a0183d2dfc" + token_contract_address_hash: "0xe18035bf8712672935fdb4e5e431b1a0183d2dfc", + token_id: nil, + token_type: "ERC-721" } ]) end diff --git a/apps/indexer/test/indexer/transform/token_transfers_test.exs b/apps/indexer/test/indexer/transform/token_transfers_test.exs index b38c1d4088..dbea962c32 100644 --- a/apps/indexer/test/indexer/transform/token_transfers_test.exs +++ b/apps/indexer/test/indexer/transform/token_transfers_test.exs @@ -74,6 +74,7 @@ defmodule Indexer.Transform.TokenTransfersTest do block_hash: log_3.block_hash }, %{ + token_id: nil, amount: Decimal.new(17_000_000_000_000_000_000), block_number: log_1.block_number, log_index: log_1.index, @@ -131,6 +132,83 @@ defmodule Indexer.Transform.TokenTransfersTest do assert TokenTransfers.parse([log]) == expected end + test "parses erc1155 token transfer" do + log = %{ + address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", + block_number: 8_683_457, + data: + "0x1000000000000c520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + first_topic: "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62", + secon_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", + third_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", + fourth_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", + index: 2, + transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", + type: "mined" + } + + assert TokenTransfers.parse([log]) == %{ + token_transfers: [ + %{ + amount: 1, + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", + block_number: 8_683_457, + from_address_hash: "0x9c978f4cfa1fe13406bcc05baf26a35716f881dd", + log_index: 2, + to_address_hash: "0x9c978f4cfa1fe13406bcc05baf26a35716f881dd", + token_contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", + token_id: + 7_237_005_577_332_282_011_952_059_972_634_123_378_909_214_838_582_411_639_295_170_840_059_424_276_480, + token_type: "ERC-1155", + transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8" + } + ], + tokens: [ + %{ + contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", + type: "ERC-1155" + } + ] + } + end + + test "parses erc1155 batch token transfer" do + log = %{ + address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", + block_number: 8_683_457, + data: + "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388", + first_topic: "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb", + secon_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", + third_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", + fourth_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", + index: 2, + transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", + type: "mined" + } + + assert TokenTransfers.parse([log]) == %{ + token_transfers: [ + %{ + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", + block_number: 8_683_457, + from_address_hash: "0x6c943470780461b00783ad530a53913bd2c104d3", + log_index: 2, + to_address_hash: "0x6c943470780461b00783ad530a53913bd2c104d3", + token_contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", + token_id: nil, + token_ids: [680_564_733_841_876_926_926_749_214_863_536_422_912], + token_type: "ERC-1155", + transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", + amounts: [5000] + } + ], + tokens: [%{contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", type: "ERC-1155"}] + } + end + test "logs error with unrecognized token transfer format" do log = %{ address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",