From e0146583985d726979db58f2ab4d787b2a4f97b8 Mon Sep 17 00:00:00 2001 From: nikitosing Date: Fri, 30 Jul 2021 14:58:00 +0700 Subject: [PATCH] Complement for 1155 --- .../tokens/instance/holder_controller.ex | 77 ++++++++++ .../controllers/tokens/token_controller.ex | 9 +- .../tokens/instance/holder/index.html.eex | 41 ++++++ .../tokens/instance/overview/_tabs.html.eex | 9 +- .../tokens/inventory/_token.html.eex | 52 ++++--- .../views/tokens/instance/holder_view.ex | 5 + .../views/tokens/instance/overview_view.ex | 1 + .../views/tokens/inventory_view.ex | 1 + .../lib/block_scout_web/web_router.ex | 7 + apps/explorer/lib/explorer/chain.ex | 34 ++++- .../chain/address/current_token_balance.ex | 138 +++++++++++++----- .../runner/address/current_token_balances.ex | 2 +- .../import/runner/address/token_balances.ex | 2 +- .../counters/token_transfers_counter.ex | 2 +- 14 files changed, 315 insertions(+), 65 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex 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..2d4bbfa442 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,9 +12,8 @@ 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) + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do + {transfer_count, token_holder_count} = fetch_token_counters(address_hash, 200) json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count}) else @@ -23,7 +22,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do 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 +30,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/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/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 fbc9cda46d..8f0baa9257 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/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index a248bacff1..90ef4bbec8 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 @@ -282,6 +282,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/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index afedb609c3..aecb6e72a6 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -5419,6 +5419,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) @@ -5539,9 +5546,34 @@ 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?(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 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 c0743cde27..e67366823a 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -9,9 +9,9 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do use Explorer.Schema import Ecto.Changeset - import Ecto.Query, only: [from: 2, limit: 2, offset: 2, order_by: 3, preload: 2, subquery: 1, where: 3] + import Ecto.Query, only: [from: 2, limit: 2, offset: 2, order_by: 3, preload: 2, where: 3] - alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, BridgedToken, Hash, Token} @default_paging_options %PagingOptions{page_size: 50} @@ -104,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) + |> 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. """ @@ -131,47 +182,60 @@ 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. Token holders cannot be the burn address (#{@burn_address_hash}) and must have a non-zero value. """ def token_holders_query(token_contract_address_hash) do - with token <- Repo.get_by(Token, contract_address_hash: token_contract_address_hash), - "ERC-20" <- token.type do - from( - tb in __MODULE__, - where: tb.token_contract_address_hash == ^token_contract_address_hash, - where: tb.address_hash != ^@burn_address_hash, - where: tb.value > 0 - ) - else - _ -> - query = - from( - tb in __MODULE__, - where: tb.token_contract_address_hash == ^token_contract_address_hash, - where: tb.address_hash != ^@burn_address_hash, - where: tb.value > 0, - windows: [ - w: [partition_by: [tb.token_contract_address_hash, tb.address_hash]] - ], - select: %__MODULE__{ - token_contract_address_hash: tb.token_contract_address_hash, - address_hash: tb.address_hash, - value: tb.value, - block_number: tb.block_number, - max_block_number: over(max(tb.block_number), :w) - } - ) - - from( - q in subquery(query), - where: q.max_block_number == q.block_number, - select: q, - distinct: q.address_hash - ) - end + from( + tb in __MODULE__, + where: tb.token_contract_address_hash == ^token_contract_address_hash, + where: tb.address_hash != ^@burn_address_hash, + where: tb.value > 0 + ) end defp page_token_balances(query, %PagingOptions{key: nil}), do: query 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 6adc433773..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 @@ -227,7 +227,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do 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) do + if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do change else Map.put(change, :token_id, nil) 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 e041d5a619..7bb6aa6757 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 @@ -67,7 +67,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do 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) do + if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do change else Map.put(change, :token_id, nil) diff --git a/apps/explorer/lib/explorer/counters/token_transfers_counter.ex b/apps/explorer/lib/explorer/counters/token_transfers_counter.ex index ccb29f7554..2026be6519 100644 --- a/apps/explorer/lib/explorer/counters/token_transfers_counter.ex +++ b/apps/explorer/lib/explorer/counters/token_transfers_counter.ex @@ -6,7 +6,7 @@ defmodule Explorer.Counters.TokenTransfersCounter do alias Explorer.Chain - @cache_name :token_holders_counter + @cache_name :token_transfers_counter @last_update_key "last_update" @ets_opts [