Complement for 1155

pull/8050/head
nikitosing 3 years ago committed by Viktor Baranov
parent 9d637793b6
commit e014658398
  1. 77
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex
  2. 9
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex
  3. 41
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex
  4. 9
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex
  5. 52
      apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex
  6. 5
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex
  7. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  8. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex
  9. 7
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  10. 34
      apps/explorer/lib/explorer/chain.ex
  11. 138
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  12. 2
      apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
  13. 2
      apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
  14. 2
      apps/explorer/lib/explorer/counters/token_transfers_counter.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

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

@ -0,0 +1,41 @@
<section class="container">
<%= render(
OverviewView,
"_details.html",
token: @token,
total_token_transfers: @total_token_transfers,
token_id: @token_instance.token_id,
token_instance: @token_instance,
conn: @conn
) %>
<section>
<div class="card">
<%= render OverviewView, "_tabs.html", assigns %>
<div class="card-body" data-async-load data-async-listing="<%= @current_path %>">
<h2 class="card-title list-title-description"><%= gettext "Token Holders" %></h2>
<div class="list-top-pagination-container-wrapper">
<%= 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 %>
</div>
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
</button>
<div data-empty-response-message class="tile tile-muted text-center" style="display: none;">
<span data-selector="empty-transactions-list">
<%= gettext "There are no transfers for this Token." %>
</span>
</div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= 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 %>
</div>
</div>
</section>
</section>

@ -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 %>
</div>

@ -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%>
<!-- use "tile-type-unique-token-image" to token with images -->
<div class="tile tile-type-unique-token fade-in">
<div class="row">
<div class="pl-5 col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center">
<!-- substitute the span with <img class="tile-image" /> to token with images -->
<span class="tile-label"><%= gettext "Unique Token" %></span>
<%= if is_unique do%>
<span class="tile-label"><%= gettext "Unique Token" %></span>
<% else %>
<span class="tile-label"><%= gettext "Not unique Token" %></span>
<% end %>
</div>
<div class="tile-content col-md-7 col-lg-8 d-flex flex-column">
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
<%= if is_unique do %>
<div class="tile-content col-md-7 col-lg-8 d-flex flex-column">
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
</span>
</span>
</span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Owner Address" %>:</span>
<span class="tile-title">
<%= render BlockScoutWeb.AddressView,
"_link.html",
address: @token_transfer.to_address,
contract: false,
use_custom_tooltip: false %>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Owner Address" %>:</span>
<span class="tile-title">
<%= render BlockScoutWeb.AddressView,
"_link.html",
address: @token_transfer.to_address,
contract: false,
use_custom_tooltip: false %>
</span>
</span>
</span>
</div>
</div>
<% else %>
<div class="tile-content col-md-7 col-lg-8 d-flex flex-column justify-content-lg-center">
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
</span>
</span>
</div>
<% end %>
<div class="pl-5 col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center">
<!-- substitute the span with <img class="tile-image" /> to token with images -->

@ -0,0 +1,5 @@
defmodule BlockScoutWeb.Tokens.Instance.HolderView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.Instance.OverviewView
end

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

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

@ -282,6 +282,13 @@ defmodule BlockScoutWeb.WebRouter do
only: [:index],
as: :metadata
)
resources(
"/token-holders",
Tokens.Instance.HolderController,
only: [:index],
as: :holder
)
end
end

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

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

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

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

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

Loading…
Cancel
Save