Complement for 1155

(cherry picked from commit e014658398)
pull/4761/head
nikitosing 3 years ago committed by Viktor Baranov
parent dd92450777
commit fac9cc0038
  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. 7
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex
  5. 18
      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. 105
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  12. 85
      apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
  13. 101
      apps/explorer/lib/explorer/chain/import/runner/address/token_balances.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>

@ -12,4 +12,11 @@
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,11 +1,18 @@
<% 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 -->
<%= if is_unique do%>
<span class="tile-label"><%= gettext "Unique Token" %></span>
<% else %>
<span class="tile-label"><%= gettext "Not unique Token" %></span>
<% end %>
</div>
<%= 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>
@ -13,7 +20,6 @@
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
</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">
@ -25,6 +31,16 @@
</span>
</span>
</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

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

@ -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,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,7 +9,7 @@ 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]
import Ecto.Query, only: [from: 2, limit: 2, offset: 2, order_by: 3, preload: 2, where: 3]
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, Block, BridgedToken, Hash, Token}
@ -96,6 +96,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.
"""
@ -137,6 +188,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.
@ -150,4 +243,14 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
where: tb.value > 0
)
end
defp page_token_balances(query, %PagingOptions{key: nil}), do: query
defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do
where(
query,
[tb],
tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash)
)
end
end

@ -201,18 +201,97 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances 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})
%{
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,
conflict_target: ~w(address_hash token_contract_address_hash)a,
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

@ -60,20 +60,111 @@ 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})
%{
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)
{:ok, _} =
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} ->
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.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,
conflict_target: ~w(address_hash token_contract_address_hash block_number)a,
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

Loading…
Cancel
Save