Add tab token holders in the Token's page

pull/668/head
Felipe Renan 6 years ago
parent b856eed594
commit e9e03120c9
  1. 21
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 36
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex
  3. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  4. 19
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex
  5. 94
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex
  6. 14
      apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex
  7. 18
      apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex
  8. 50
      apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
  9. 92
      apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
  10. 23
      apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex
  11. 22
      apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
  12. 40
      apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs

@ -12,7 +12,18 @@ defmodule BlockScoutWeb.Chain do
string_to_transaction_hash: 1 string_to_transaction_hash: 1
] ]
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, TokenTransfer, Transaction} alias Explorer.Chain.{
Address,
Address.TokenBalance,
Block,
Hash,
InternalTransaction,
Log,
Token,
TokenTransfer,
Transaction
}
alias Explorer.PagingOptions alias Explorer.PagingOptions
@page_size 50 @page_size 50
@ -121,6 +132,10 @@ defmodule BlockScoutWeb.Chain do
def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}), def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}),
do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}] do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}]
def paging_options(%{"value" => value, "address_hash" => address_hash}) do
[paging_options: %{@default_paging_options | key: {value, address_hash}}]
end
def paging_options(_params), do: [paging_options: @default_paging_options] def paging_options(_params), do: [paging_options: @default_paging_options]
def param_to_block_number(formatted_number) when is_binary(formatted_number) do def param_to_block_number(formatted_number) when is_binary(formatted_number) do
@ -179,6 +194,10 @@ defmodule BlockScoutWeb.Chain do
%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime} %{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime}
end end
defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do
%{"address_hash" => Hash.to_string(address_hash), "value" => Decimal.to_integer(value)}
end
defp transaction_from_param(param) do defp transaction_from_param(param) do
with {:ok, hash} <- string_to_transaction_hash(param) do with {:ok, hash} <- string_to_transaction_hash(param) do
hash_to_transaction(hash) hash_to_transaction(hash)

@ -0,0 +1,36 @@
defmodule BlockScoutWeb.Tokens.HolderController do
use BlockScoutWeb, :controller
alias Explorer.Chain
import BlockScoutWeb.Chain,
only: [
split_list_by_page: 1,
paging_options: 1,
next_page_params: 3
]
def index(conn, %{"token_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash),
token_balances <- Chain.fetch_token_holders_from_token_hash(address_hash, paging_options(params)) do
{token_balances_paginated, next_page} = split_list_by_page(token_balances)
render(
conn,
"index.html",
token: token,
token_balances: token_balances_paginated,
total_address_in_token_transfers: Chain.count_addresses_in_token_transfers_from_token_hash(address_hash),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
next_page_params: next_page_params(next_page, token_balances_paginated, params)
)
else
:error ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -113,6 +113,13 @@ defmodule BlockScoutWeb.Router do
only: [:index], only: [:index],
as: :read_contract as: :read_contract
) )
resources(
"/token_holders",
Tokens.HolderController,
only: [:index],
as: :holder
)
end end
resources( resources(

@ -0,0 +1,19 @@
<div class="tile tile-type-token fade-in mb-10" data-test="token_holders">
<div class="row">
<div class="col-md-7 col-lg-8 d-flex flex-column">
<span>
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @token_balance.address_hash, contract: BlockScoutWeb.AddressView.contract?(@token_balance.address) %>
</span>
<span>
<span class="text-dark">
<%= format_token_balance_value(@token_balance.value, @token) %> <%= @token.symbol %>
</span>
<%= if @token.total_supply > 0 do %>
(<%= total_supply_percentage(@token_balance.value, @token.total_supply) %>)
<% end %>
</span>
</div>
</div>
</div>

@ -0,0 +1,94 @@
<section class="container">
<%= render(
OverviewView,
"_details.html",
token: @token,
total_token_transfers: @total_token_transfers,
total_address_in_token_transfers: @total_address_in_token_transfers
) %>
<section>
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<li class="nav-item">
<%= link(
gettext("Token Transfers"),
class: "nav-link",
to: token_path(@conn, :show, @token.contract_address_hash)
) %>
</li>
<%= if TokenView.smart_contract_with_read_only_functions?(@token) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: token_read_contract_path(@conn, :index, @conn.params["id"]),
class: "nav-link")%>
</li>
<% end %>
<li class="nav-item">
<%= link(
gettext("Token Holders"),
class: "nav-link active",
"data-test": "token_holders_tab",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</li>
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Token Holders") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
to: token_path(@conn, :show, @token.contract_address_hash)
) %>
<%= if TokenView.smart_contract_with_read_only_functions?(@token) do %>
<%= link(
gettext("Read Contract"),
to: "#",
class: "dropdown-item")%>
<% end %>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div>
</li>
</ul>
</div>
<!-- Token Holders -->
<div class="card-body">
<h2 class="card-title"><%= gettext "Token Holders" %></h2>
<%= if Enum.any?(@token_balances) do %>
<%= for token_balance <- @token_balances do %>
<%= render "_token_balances.html", token: @token, token_balance: token_balance %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
<span data-selector="empty-transactions-list">
<%= gettext "There are no holders for this Token." %>
</span>
</div>
<% end %>
<%= if @next_page_params do %>
<%= link(
gettext("Next Page"),
class: "button button-secondary button-small float-right mt-4",
to: token_holder_path(@conn, :index, @token.contract_address_hash, @next_page_params)
) %>
<% end %>
</div>
</div>
</section>
</section>

@ -26,6 +26,15 @@
to: token_read_contract_path(@conn, :index, @conn.params["token_id"]), to: token_read_contract_path(@conn, :index, @conn.params["token_id"]),
class: "nav-link active")%> class: "nav-link active")%>
</li> </li>
<li class="nav-item">
<%= link(
gettext("Token Holders"),
class: "nav-link active",
"data-test": "token_holders_tab",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</li>
</ul> </ul>
<!-- MOBILE DROPDOWN NAV --> <!-- MOBILE DROPDOWN NAV -->
@ -42,6 +51,11 @@
gettext("Read Contract"), gettext("Read Contract"),
to: "#", to: "#",
class: "nav-link")%> class: "nav-link")%>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div> </div>
</li> </li>
</ul> </ul>

@ -28,6 +28,15 @@
class: "nav-link")%> class: "nav-link")%>
</li> </li>
<% end %> <% end %>
<li class="nav-item"i>
<%= link(
gettext("Token Holders"),
class: "nav-link",
"data-test": "token_holders_tab",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</li>
</ul> </ul>
<!-- MOBILE DROPDOWN NAV --> <!-- MOBILE DROPDOWN NAV -->
@ -37,15 +46,20 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
<%= link( <%= link(
gettext("Token Transfers"), gettext("Token Transfers"),
class: "nav-link active", class: "dropdown-item",
to: token_path(@conn, :show, @token.contract_address_hash) to: token_path(@conn, :show, @token.contract_address_hash)
) %> ) %>
<%= if smart_contract_with_read_only_functions?(@token) do %> <%= if smart_contract_with_read_only_functions?(@token) do %>
<%= link( <%= link(
gettext("Read Contract"), gettext("Read Contract"),
to: "#", to: "#",
class: "nav-link")%> class: "dropdown-item")%>
<% end %> <% end %>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div> </div>
</li> </li>
</ul> </ul>

@ -0,0 +1,50 @@
defmodule BlockScoutWeb.Tokens.HolderView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.{OverviewView, TokenView}
alias Explorer.Chain.{Token}
@doc """
Calculates the percentage of the value from the given total supply.
## Examples
iex> value = Decimal.new(200)
iex> total_supply = Decimal.new(1000)
iex> BlockScoutWeb.Tokens.HolderView.total_supply_percentage(value, total_supply)
"20.0000%"
"""
def total_supply_percentage(value, total_supply) do
result =
value
|> Decimal.div(total_supply)
|> Decimal.mult(100)
|> Decimal.round(4)
|> Decimal.to_string()
result <> "%"
end
@doc """
Formats the token balance value according to the Token's type.
## Examples
iex> token = build(:token, type: "ERC-20", decimals: 2)
iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, token)
"1,000"
iex> token = build(:token, type: "ERC-721")
iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, token)
1
"""
def format_token_balance_value(value, %Token{type: "ERC-20", decimals: decimals}) do
format_according_to_decimals(value, decimals)
end
def format_token_balance_value(value, _token) do
value
end
end

@ -0,0 +1,92 @@
defmodule BlockScoutWeb.Tokens.HolderControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.Hash
describe "GET index/3" do
test "with invalid address hash", %{conn: conn} do
conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, "invalid_address"))
assert html_response(conn, 404)
end
test "with a token that doesn't exist", %{conn: conn} do
address = build(:address)
conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, address.hash))
assert html_response(conn, 404)
end
test "successfully renders the page", %{conn: conn} do
token = insert(:token)
insert_list(
2,
:token_balance,
token_contract_address_hash: token.contract_address_hash
)
conn =
get(
conn,
token_holder_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)
)
assert html_response(conn, 200)
end
test "returns next page of results based on last seen token balance", %{conn: conn} do
contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493")
token = insert(:token, contract_address: contract_address)
second_page_token_balances =
1..50
|> Enum.map(
&insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
value: &1 + 1000
)
)
|> Enum.map(& &1.value)
token_balance =
insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
value: 50000
)
conn =
get(conn, token_holder_path(conn, :index, token.contract_address_hash), %{
"value" => Decimal.to_integer(token_balance.value),
"address_hash" => Hash.to_string(token_balance.address_hash)
})
actual_token_balances =
conn.assigns.token_balances
|> Enum.map(& &1.value)
|> Enum.reverse()
assert second_page_token_balances == actual_token_balances
end
test "next_page_params exists if not on last page", %{conn: conn} do
contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493")
token = insert(:token, contract_address: contract_address)
Enum.each(
1..51,
&insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
value: &1 + 1000
)
)
conn = get(conn, token_holder_path(conn, :index, token.contract_address_hash))
assert conn.assigns.next_page_params
end
end
end

@ -0,0 +1,23 @@
defmodule BlockScoutWeb.TokenPage do
@moduledoc false
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.{Address}
def visit_page(session, %Address{hash: address_hash}) do
visit_page(session, address_hash)
end
def visit_page(session, contract_address_hash) do
visit(session, "tokens/#{contract_address_hash}")
end
def click_tokens_holders(session) do
click(session, css("[data-test='token_holders_tab']"))
end
def token_holders(count: count) do
css("[data-test='token_holders']", count: count)
end
end

@ -0,0 +1,22 @@
defmodule BlockScoutWeb.ViewingTokensTest do
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.TokenPage
describe "viewing token holders" do
test "list the token holders", %{session: session} do
token = insert(:token)
insert_list(
2,
:token_balance,
token_contract_address_hash: token.contract_address_hash
)
session
|> TokenPage.visit_page(token.contract_address)
|> TokenPage.click_tokens_holders()
|> assert_has(TokenPage.token_holders(count: 2))
end
end
end

@ -0,0 +1,40 @@
defmodule BlockScoutWeb.Tokens.HolderViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.Tokens.HolderView
alias Explorer.Chain.{Address.TokenBalance, Token}
doctest BlockScoutWeb.Tokens.HolderView, import: true
describe "total_supply_percentage/2" do
test "returns the percentage of the Token total supply" do
%Token{total_supply: total_supply} = build(:token, total_supply: 1000)
%TokenBalance{value: value} = build(:token_balance, value: 200)
assert HolderView.total_supply_percentage(value, total_supply) == "20.0000%"
end
test "considers 4 decimals" do
%Token{total_supply: total_supply} = build(:token, total_supply: 100_000_009)
%TokenBalance{value: value} = build(:token_balance, value: 500)
assert HolderView.total_supply_percentage(value, total_supply) == "0.0005%"
end
end
describe "format_token_balance_value/1" do
test "formats according to token decimals when it's a ERC-20" do
token = build(:token, type: "ERC-20", decimals: 2)
token_balance = build(:token_balance, value: 2_000_000)
assert HolderView.format_token_balance_value(token_balance.value, 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
end
end
end
Loading…
Cancel
Save